From 815e734039dd05566c78d7ce2fdcc906466a203f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 21 Jun 2024 14:58:58 +0300 Subject: [PATCH 01/44] :sparkles: Enumeration asset type to create lists of predefined values for content types, behaviors, and to be used directly in code --- app/data/i18n/English.json | 57 +++--- src/ct.release/index.ts | 2 + src/js/projectMigrationScripts/5.0.2.js | 10 + src/node_requires/catnip/blocks.d.ts | 12 +- src/node_requires/catnip/compiler.ts | 22 ++- .../catnip/stdLib/hiddenBlocks.ts | 16 ++ src/node_requires/exporter/enums.ts | 18 ++ src/node_requires/exporter/index.ts | 7 +- .../exporter/scriptableProcessor.ts | 4 +- src/node_requires/exporter/utils.ts | 13 +- src/node_requires/platformUtils.ts | 1 + src/node_requires/resources/commonTypes.d.ts | 3 +- .../resources/content/IFieldSchema.d.ts | 2 + src/node_requires/resources/content/index.ts | 184 +++++++++++------- src/node_requires/resources/enums/IEnum.d.ts | 4 + src/node_requires/resources/enums/index.ts | 41 ++++ src/node_requires/resources/index.ts | 11 +- .../resources/projects/defaultProject.ts | 3 +- src/node_requires/resources/projects/index.ts | 5 +- src/riotTags/catnip/catnip-block-list.tag | 1 + src/riotTags/catnip/catnip-block.tag | 18 +- src/riotTags/catnip/catnip-library.tag | 41 +++- src/riotTags/editors/enum-editor.tag | 72 +++++++ src/riotTags/shared/extensions-editor.tag | 17 +- .../tags/shared/scriptables/catnip-block.styl | 8 + 25 files changed, 456 insertions(+), 116 deletions(-) create mode 100644 src/js/projectMigrationScripts/5.0.2.js create mode 100644 src/node_requires/exporter/enums.ts create mode 100644 src/node_requires/resources/enums/IEnum.d.ts create mode 100644 src/node_requires/resources/enums/index.ts create mode 100644 src/riotTags/editors/enum-editor.tag diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index ebd57d347..e59db9843 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -122,30 +122,10 @@ "behaviors", "behaviors" ], - "texture": [ - "texture", - "textures", - "textures" - ], - "template": [ - "template", - "templates", - "templates" - ], - "tandem": [ - "emitter tandem", - "emitter tandems", - "emitter tandems" - ], - "room": [ - "room", - "rooms", - "rooms" - ], - "typeface": [ - "typeface", - "typefaces", - "typefaces" + "enum": [ + "enumeration", + "enumerations", + "enumerations" ], "script": [ "script", @@ -166,6 +146,31 @@ "skeletal sprite", "skeletal sprites", "skeletal sprites" + ], + "tandem": [ + "emitter tandem", + "emitter tandems", + "emitter tandems" + ], + "texture": [ + "texture", + "textures", + "textures" + ], + "template": [ + "template", + "templates", + "templates" + ], + "room": [ + "room", + "rooms", + "rooms" + ], + "typeface": [ + "typeface", + "typefaces", + "typefaces" ] } }, @@ -676,6 +681,10 @@ "documentation": "Documentation", "reference": "Reference" }, + "enumEditor": { + "addVariant": "Add a variant", + "enumUseCases": "This enumeration will be available across all your code and can act as a data type in content schemas and behaviors' fields." + }, "exportPanel": { "hide": "Hide", "working": "Working…", diff --git a/src/ct.release/index.ts b/src/ct.release/index.ts index 3ce4570dd..c3f4da1cb 100644 --- a/src/ct.release/index.ts +++ b/src/ct.release/index.ts @@ -303,6 +303,8 @@ let loading: Promise; (window as any).PIXI = PIXI; mountErrorListener(); +/*!@enums@*/ + /*!@globalVars@*/ { diff --git a/src/js/projectMigrationScripts/5.0.2.js b/src/js/projectMigrationScripts/5.0.2.js new file mode 100644 index 000000000..ae7da8b85 --- /dev/null +++ b/src/js/projectMigrationScripts/5.0.2.js @@ -0,0 +1,10 @@ +window.migrationProcess = window.migrationProcess || []; + +window.migrationProcess.push({ + version: '5.0.2', + process: project => new Promise(resolve => { + // Enum asset type appeared + project.settings.export.bundleAssetTypes ??= false; + resolve(); + }) +}); diff --git a/src/node_requires/catnip/blocks.d.ts b/src/node_requires/catnip/blocks.d.ts index fcde2f13d..d61034b25 100644 --- a/src/node_requires/catnip/blocks.d.ts +++ b/src/node_requires/catnip/blocks.d.ts @@ -35,9 +35,19 @@ declare interface IBlockPieceBreak { type: 'break' } +/** + * A block will need to read and write to block values' variableName property. + */ declare interface IBlockPropOrVariable { type: 'propVar'; } +/** + * A block will need to read and write to block values' enumId and enumValue properties. + */ +declare interface IBlockEnumValue { + type: 'enumValue'; +} + declare interface IBlockFiller { type: 'filler' } @@ -61,7 +71,7 @@ declare interface IBlockOptions { declare type blockPiece = IBlockPieceLabel | IBlockPieceIcon | IBlockPieceCode | IBlockPieceArgument | IBlockPieceTextbox | IBlockPieceBlocks | IBlockPropOrVariable | IBlockFiller | IBlockAsyncMarker | - IBlockPieceBreak | IBlockOptions; + IBlockPieceBreak | IBlockOptions | IBlockEnumValue; // eslint-disable-next-line no-use-before-define type argumentValues = Record; diff --git a/src/node_requires/catnip/compiler.ts b/src/node_requires/catnip/compiler.ts index c78d0f4e2..26e75f196 100644 --- a/src/node_requires/catnip/compiler.ts +++ b/src/node_requires/catnip/compiler.ts @@ -108,22 +108,34 @@ export const compile = (blocks: BlockScript, failureMeta: { } const values: Record = {}; for (const piece of declaration.pieces) { - if (piece.type === 'argument') { + // eslint-disable-next-line default-case + switch (piece.type) { + case 'argument': writeArgumentlike(piece, block.values, values, declaration, failureMeta); - } else if (piece.type === 'code' || piece.type === 'textbox') { + break; + case 'code': + case 'textbox': values[piece.key] = String(block.values[piece.key] ?? ''); - } else if (piece.type === 'options') { + break; + case 'options': for (const option of piece.options) { writeArgumentlike(option, block.values, values, declaration, failureMeta); } - } else if (piece.type === 'blocks') { + break; + case 'blocks': { const associatedVal = block.values[piece.key]; values[piece.key] = compile(associatedVal as IBlock[] ?? [], failureMeta); if (values[piece.key] === 'undefined') { values[piece.key] = ''; } - } else if (piece.type === 'propVar') { + } break; + case 'propVar': values.variableName = block.values.variableName as string; + break; + case 'enumValue': + values.enumId = block.values.enumId as string; + values.enumValue = block.values.enumValue as string; + break; } } safeId++; diff --git a/src/node_requires/catnip/stdLib/hiddenBlocks.ts b/src/node_requires/catnip/stdLib/hiddenBlocks.ts index 8a1267d41..2e458b8e4 100644 --- a/src/node_requires/catnip/stdLib/hiddenBlocks.ts +++ b/src/node_requires/catnip/stdLib/hiddenBlocks.ts @@ -1,3 +1,6 @@ +import {getTypescriptEnumName} from '../../resources/enums'; +import {getById} from 'src/node_requires/resources'; + const blocks: (IBlockCommandDeclaration | IBlockComputedDeclaration)[] = [{ name: 'Variable', hideLabel: true, @@ -92,6 +95,19 @@ const blocks: (IBlockCommandDeclaration | IBlockComputedDeclaration)[] = [{ type: 'propVar' }], jsTemplate: (values) => `content['${values.variableName}']` +}, { + name: 'Enum value', + hideLabel: true, + type: 'computed', + typeHint: 'number', + lib: 'core.hidden', + code: 'enum value', + i18nKey: 'enum value', + icon: 'list', + pieces: [{ + type: 'enumValue' + }], + jsTemplate: (values) => `${getTypescriptEnumName(getById('enum', values.enumId))}.${values.enumValue}` }]; export default blocks; diff --git a/src/node_requires/exporter/enums.ts b/src/node_requires/exporter/enums.ts new file mode 100644 index 000000000..2228b1697 --- /dev/null +++ b/src/node_requires/exporter/enums.ts @@ -0,0 +1,18 @@ +import {transform} from 'sucrase'; +import {getAllEnumsTypescript, getTypescriptEnumName} from '../resources/enums'; +import {getOfType} from '../resources'; + +export const compileEnums = (production: boolean): string => { + let output = transform(getAllEnumsTypescript(), { + transforms: ['typescript'] + }).code; + if (production) { + return output; + } + const enums = getOfType('enum'); + output += '\n' + enums.map(e => { + const tsName = getTypescriptEnumName(e); + return `window.${tsName} = ${tsName};`; + }).join('\n'); + return output; +}; diff --git a/src/node_requires/exporter/index.ts b/src/node_requires/exporter/index.ts index 246898d72..fa3f4b36a 100644 --- a/src/node_requires/exporter/index.ts +++ b/src/node_requires/exporter/index.ts @@ -22,6 +22,7 @@ import {bundleFonts, bakeBitmapFonts} from './fonts'; import {getAssetTree} from './assetTree'; import {bakeFavicons} from './icons'; import {getUnwrappedExtends, getCleanKey} from './utils'; +import {compileEnums} from './enums'; import {revHash} from './../utils/revHash'; import {substituteHtmlVars} from './html'; import {stringifyScripts, getStartupScripts} from './scripts'; @@ -366,6 +367,7 @@ const exportCtProject = async ( behaviorsTemplates: behaviors.templates, behaviorsRooms: behaviors.rooms, templates: templates.libCode, + enums: compileEnums(production), styles: stringifyStyles(assets.style), tandemTemplates: stringifyTandems(assets.tandem), fonts: typefaces.js, @@ -374,7 +376,10 @@ const exportCtProject = async ( userScripts, scriptAssets: stringifyScripts(assets.script), startupScripts: getStartupScripts(assets.script), - catmods: await modulesTask + catmods: await modulesTask, + + production, + debug: !production }, injections); diff --git a/src/node_requires/exporter/scriptableProcessor.ts b/src/node_requires/exporter/scriptableProcessor.ts index 3896c276a..95ad4f874 100644 --- a/src/node_requires/exporter/scriptableProcessor.ts +++ b/src/node_requires/exporter/scriptableProcessor.ts @@ -8,7 +8,7 @@ import {getModulePathByName, loadModuleByName} from './../resources/modules'; import {join} from 'path'; import {embedStaticBehaviors} from './behaviors'; const compileCoffee = require('coffeescript').CoffeeScript.compile; -const typeScript = require('sucrase').transform; +import {transform} from 'sucrase'; import {compile, resetSafeId} from '../catnip/compiler'; export const coffeeScriptOptions = { @@ -107,7 +107,7 @@ const getBaseScripts = function (entity: IScriptable, project: IProject): Script resetSafeId(); } else if (project.language === 'typescript') { if ((code as string).trim()) { - ({code} = typeScript(code, { + ({code} = transform(code as string, { transforms: ['typescript'] })); } else { diff --git a/src/node_requires/exporter/utils.ts b/src/node_requires/exporter/utils.ts index 77c534afe..fb25457a2 100644 --- a/src/node_requires/exporter/utils.ts +++ b/src/node_requires/exporter/utils.ts @@ -82,6 +82,17 @@ export const getUnwrappedBySpec = ( // Skip unset values continue; } + // Turn enum entries' names into numerical constant equivalent to this enum's value. + if (fieldMap[i].type.startsWith('enum@')) { + const [, id] = fieldMap[i].type.split('@'); + const enumAsset = getById('enum', id); + if (fieldMap[i].array) { + out[i] = (exts[i] as string[]).map(elt => enumAsset.values.indexOf(elt)); + } else { + out[i] = enumAsset.values.indexOf(exts[i] as string); + } + continue; + } if (unwrappable.includes(fieldMap[i].type)) { if (fieldMap[i].array) { out[i] = (exts[i] as string[]).map(elt => { @@ -99,7 +110,7 @@ export const getUnwrappedBySpec = ( continue; } try { - out[i] = getById(fieldMap[i].type as 'template' | 'texture', String(exts[i])).name; + out[i] = getById(fieldMap[i].type as resourceType, String(exts[i])).name; } catch (e) { alertify.error(`Could not resolve UID ${exts[i]} for field ${i} as a ${fieldMap[i].type}. Returning -1. Full object: ${JSON.stringify(exts)}`); console.error(e); diff --git a/src/node_requires/platformUtils.ts b/src/node_requires/platformUtils.ts index 4692f949e..4afaf6880 100644 --- a/src/node_requires/platformUtils.ts +++ b/src/node_requires/platformUtils.ts @@ -24,6 +24,7 @@ const {$} = require('execa'); console.debug(`Detected node.js ${stdout} installed in the system.`); } catch (e) { isNodeInstalled = false; + // eslint-disable-next-line no-console console.debug('Could not detect node.js in the system.'); } })(); diff --git a/src/node_requires/resources/commonTypes.d.ts b/src/node_requires/resources/commonTypes.d.ts index 4ce88e9f7..72638d0b6 100644 --- a/src/node_requires/resources/commonTypes.d.ts +++ b/src/node_requires/resources/commonTypes.d.ts @@ -1,5 +1,6 @@ type resourceType = 'template' | 'room' | 'sound' | 'style' | - 'texture' | 'tandem' | 'typeface' | 'behavior' | 'script'; + 'texture' | 'tandem' | 'typeface' | 'behavior' | 'script' | + 'enum'; type fontWeight = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; diff --git a/src/node_requires/resources/content/IFieldSchema.d.ts b/src/node_requires/resources/content/IFieldSchema.d.ts index 85271d1ef..d79f53e45 100644 --- a/src/node_requires/resources/content/IFieldSchema.d.ts +++ b/src/node_requires/resources/content/IFieldSchema.d.ts @@ -1,3 +1,5 @@ +type enumId = `enum@${id}`; + declare interface IFieldSchema { name: string, readableName: string, diff --git a/src/node_requires/resources/content/index.ts b/src/node_requires/resources/content/index.ts index 46ebd180c..fbb0bbf51 100644 --- a/src/node_requires/resources/content/index.ts +++ b/src/node_requires/resources/content/index.ts @@ -1,54 +1,68 @@ import {getByPath} from '../../i18n'; -import {assetTypes, exists} from '..'; +import {assetTypes, exists, getById, getOfType} from '..'; +import {getTypescriptEnumName} from '../enums'; const capitalize = (str: string): string => str.slice(0, 1).toUpperCase() + str.slice(1); -export const getFieldsExtends = (): IExtensionField[] => [{ - name: getByPath('settings.content.typeSpecification') as string, - type: 'table', - key: 'specification', - fields: [{ - name: getByPath('settings.content.fieldName'), - type: 'text', - key: 'name', - help: getByPath('settings.content.fieldNameHint') - }, { - name: getByPath('settings.content.fieldReadableName'), - type: 'text', - key: 'readableName', - help: getByPath('settings.content.fieldReadableNameHint') - }, { - name: getByPath('settings.content.fieldType'), - type: 'select', - key: 'type', - options: ['text', 'textfield', 'code', '', 'number', 'sliderAndNumber', 'point2D', '', ...assetTypes, '', 'checkbox', 'color'].map(type => ({ - // eslint-disable-next-line no-nested-ternary - name: type === '' ? - '' : - (assetTypes.includes(type as resourceType) ? - capitalize(getByPath(`common.assetTypes.${type}.0`) as string) : - getByPath('common.fieldTypes.' + type) - ), - value: type - })), - default: 'text' - }, { - name: getByPath('common.required'), - type: 'checkbox', - key: 'required', - default: false - }, { - name: getByPath('settings.content.array'), - type: 'checkbox', - key: 'array', - default: false - }, { - name: getByPath('settings.content.fixedLength'), - type: 'number', - key: 'fixedLength', - if: 'array' - }] as IExtensionField[] -}]; +export const getFieldsExtends = (): IExtensionField[] => { + const enums = getOfType('enum'); + const defaultFieldTypes = ['text', 'textfield', 'code', '', 'number', 'sliderAndNumber', 'point2D', '', ...assetTypes, '', 'checkbox', 'color']; + if (getOfType('enum').length) { + defaultFieldTypes.push(''); + } + const fieldTypeOptions = defaultFieldTypes.map(type => ({ + // eslint-disable-next-line no-nested-ternary + name: type === '' ? + '' : + (assetTypes.includes(type as resourceType) ? + capitalize(getByPath(`common.assetTypes.${type}.0`) as string) : + getByPath('common.fieldTypes.' + type) + ), + value: type + })); + fieldTypeOptions.push(...enums.map(enumAsset => ({ + name: enumAsset.name, + value: `enum@${enumAsset.uid}` + }))); + const options: IExtensionField[] = [{ + name: getByPath('settings.content.typeSpecification') as string, + type: 'table', + key: 'specification', + fields: [{ + name: getByPath('settings.content.fieldName'), + type: 'text', + key: 'name', + help: getByPath('settings.content.fieldNameHint') + }, { + name: getByPath('settings.content.fieldReadableName'), + type: 'text', + key: 'readableName', + help: getByPath('settings.content.fieldReadableNameHint') + }, { + name: getByPath('settings.content.fieldType'), + type: 'select', + key: 'type', + options: fieldTypeOptions, + default: 'text' + }, { + name: getByPath('common.required'), + type: 'checkbox', + key: 'required', + default: false + }, { + name: getByPath('settings.content.array'), + type: 'checkbox', + key: 'array', + default: false + }, { + name: getByPath('settings.content.fixedLength'), + type: 'number', + key: 'fixedLength', + if: 'array' + }] as IExtensionField[] + }]; + return options; +}; export const getExtends = (): IExtensionField[] => [{ name: getByPath('settings.content.typeName') as string, @@ -75,6 +89,7 @@ export const fieldTypeToTsType: Record = { number: 'number', point2D: '[number, number]', sliderAndNumber: 'number', + icon: 'string', text: 'string', room: 'string', sound: 'string', @@ -86,13 +101,19 @@ export const fieldTypeToTsType: Record = { typeface: 'string', script: 'string', style: 'string', - icon: 'string' + enum: 'string' }; +const getFieldsTsType = (field: IFieldSchema): string => { + if (!field.type.startsWith('enum@')) { + return fieldTypeToTsType[field.type]; + } + return getTypescriptEnumName(getById('enum', field.type.split('@')[1])); +}; const getTsType = (content: IContentType): string => { const fields = content.specification .map(f => ` /**${f.readableName || f.name}*/ - '${f.name}': ${fieldTypeToTsType[f.type]}${f.array ? '[]' : ''};`) + '${f.name}': ${getFieldsTsType(f)}${f.array ? '[]' : ''};`) .join('\n'); return ` var ${content.name}: { @@ -192,7 +213,38 @@ const validationTypeMap: Record validateRef(val, 'style'), () => -1], tandem: [val => validateRef(val, 'tandem'), () => -1], template: [val => validateRef(val, 'template'), () => -1], - texture: [val => validateRef(val, 'texture'), () => -1] + texture: [val => validateRef(val, 'texture'), () => -1], + enum: [val => validateRef(val, 'enum'), () => -1] +}; + +const enumValidatorTuple: [ + (val: unknown, enumAsset: IEnum) => boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (enumAsset: IEnum) => string +] = [ + (v, enumAsset) => enumAsset.values.includes(v as string), + enumAsset => enumAsset.values[0] +]; + +/** + * Checks a primitive value against its type and resets it to its default value if it is invalid. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const validateValue = (obj: any[] | any, key: string | number, fieldType: directlyValidated | `enum@${string}`): void => { + const val = obj[key]; + if (fieldType.startsWith('enum@')) { + const [, id] = fieldType.split('@'); + const enumAsset = getById('enum', id); + if (!enumValidatorTuple[0](val, enumAsset)) { + obj[key] = enumValidatorTuple[1](enumAsset); + } + } else { + // Get the validation function and the default value getter for this field type. + const [validator, defaultValue] = validationTypeMap[fieldType as directlyValidated]; + if (!validator(val)) { + obj[key] = defaultValue(); + } + } }; /** @@ -206,20 +258,16 @@ export const validateContentEntries = ( for (const field of schema) { let val = target[field.name]; const ftype = field.type; - - // Get the validation function and the default value getter for this field type. - const [validator, defaultValue] = validationTypeMap[ftype]; - if (field.array) { if (!Array.isArray(val)) { - target[field.name] = []; - val = target[field.name]; + val = target[field.name] = []; } const elts = val as unknown[]; - target[field.name] = elts - .map(v => (validator(v) ? v : defaultValue())); - } else if (!validator(val)) { - target[field.name] = defaultValue(); + for (let i = 0; i < elts.length; i++) { + validateValue(elts, i, ftype); + } + } else { + validateValue(target, field.name, ftype); } } }; @@ -236,10 +284,12 @@ export const validateExtends = ( continue; } if (extension.type === 'array') { - target[extension.key] = target[extension.key] || []; - const [validator, defaultValue] = validationTypeMap[(extension.arrayType ?? 'text') as directlyValidated]; - target[extension.key] = (target[extension.key] as unknown[]) - .map(elt => (validator(elt, extension) ? elt : defaultValue(extension))); + if (!Array.isArray(target[extension.key])) { + target[extension.key] = []; + } + for (let i = 0; i < (target[extension.key] as unknown[]).length; i++) { + validateValue(target[extension.key], i, extension.arrayType! as directlyValidated); + } } else if (extension.type === 'group' && extension.items) { if (typeof target[extension.key] !== 'object' || Array.isArray(target[extension.key])) { target[extension.key] = {}; @@ -251,11 +301,7 @@ export const validateExtends = ( validateExtends(extension.fields, row); } } else { - const [validator, defaultValue] = - validationTypeMap[extension.type as directlyValidated]; - target[extension.key] = validator(target[extension.key], extension) ? - target[extension.key] : - defaultValue(extension); + validateValue(target, extension.key, extension.type as directlyValidated); } } }; diff --git a/src/node_requires/resources/enums/IEnum.d.ts b/src/node_requires/resources/enums/IEnum.d.ts new file mode 100644 index 000000000..ff8ffdebf --- /dev/null +++ b/src/node_requires/resources/enums/IEnum.d.ts @@ -0,0 +1,4 @@ +interface IEnum extends IAsset { + type: 'enum'; + values: string[]; +} diff --git a/src/node_requires/resources/enums/index.ts b/src/node_requires/resources/enums/index.ts new file mode 100644 index 000000000..00efec7c4 --- /dev/null +++ b/src/node_requires/resources/enums/index.ts @@ -0,0 +1,41 @@ +import generateGUID from '../../generateGUID'; +import {getOfType} from '..'; +import {IDisposable, languages} from 'monaco-editor'; +import {promptName} from '../promptName'; + +export const getTypescriptEnumName = (enumType: IEnum): string => enumType.name.replace(/\s/g, '_'); +export const getTypescriptForEnum = (enumType: IEnum): string => `enum ${getTypescriptEnumName(enumType)} { +${enumType.values.map((v, ind) => ` ${v} = ${ind},`).join('\n')} +};`; + +export const getAllEnumsTypescript = (): string => getOfType('enum') + .map(getTypescriptForEnum) + .join('\n'); + +let enumsDisposable: IDisposable | null = null; +export const updateEnumsTs = (): void => { + if (enumsDisposable) { + enumsDisposable.dispose(); + } + enumsDisposable = languages.typescript.typescriptDefaults.addExtraLib(getAllEnumsTypescript()); +}; +window.signals.on('enumChanged', updateEnumsTs); + +export const areThumbnailsIcons = true; +export const getThumbnail = () => 'list'; + +export const createAsset = async (): Promise => { + const name = await promptName('enum', 'NewEnumeration'); + if (!name) { + // eslint-disable-next-line no-throw-literal + throw 'cancelled'; + } + const enumAsset = { + name, + values: ['Value1'], + lastmod: Number(new Date()), + type: 'enum' as const, + uid: generateGUID() + }; + return enumAsset; +}; diff --git a/src/node_requires/resources/index.ts b/src/node_requires/resources/index.ts index 8dfbaa74f..acaaeff65 100644 --- a/src/node_requires/resources/index.ts +++ b/src/node_requires/resources/index.ts @@ -9,6 +9,7 @@ import * as sounds from './sounds'; import * as styles from './styles'; import * as templates from './templates'; import * as textures from './textures'; +import * as enums from './enums'; import getUid from '../generateGUID'; import {getLanguageJSON, getByPath} from '../i18n'; @@ -72,7 +73,8 @@ const typeToApiMap: Record = { template: templates, texture: textures, behavior: behaviors, - script: scripts + script: scripts, + enum: enums }; /** Names of all possible asset types */ export const assetTypes = Object.keys(typeToApiMap) as resourceType[]; @@ -87,6 +89,7 @@ type typeToTsTypeMap = { T extends 'tandem' ? ITandem : T extends 'template' ? ITemplate : T extends 'behavior' ? IBehavior : + T extends 'enum'? IEnum : T extends 'script' ? IScript : never; } @@ -523,7 +526,8 @@ export const resourceToIconMap: Record = { style: 'ui', script: 'code-alt', // skeleton: 'skeletal-animation', - behavior: 'behavior' + behavior: 'behavior', + enum: 'list' }; export const editorMap: Record = { typeface: 'typeface-editor', @@ -535,7 +539,8 @@ export const editorMap: Record = { template: 'template-editor', texture: 'texture-editor', behavior: 'behavior-editor', - script: 'script-editor' + script: 'script-editor', + enum: 'enum-editor' }; export { diff --git a/src/node_requires/resources/projects/defaultProject.ts b/src/node_requires/resources/projects/defaultProject.ts index a692ef816..18c34eb56 100644 --- a/src/node_requires/resources/projects/defaultProject.ts +++ b/src/node_requires/resources/projects/defaultProject.ts @@ -58,7 +58,8 @@ const defaultProjectTemplate: IProject = { sound: false, style: false, tandem: false, - script: false + script: false, + enum: false } }, branding: { diff --git a/src/node_requires/resources/projects/index.ts b/src/node_requires/resources/projects/index.ts index 7f80e107c..d09b22df1 100644 --- a/src/node_requires/resources/projects/index.ts +++ b/src/node_requires/resources/projects/index.ts @@ -7,6 +7,7 @@ import {buildAssetMap} from '..'; import {preparePreviews} from '../preview'; import {refreshFonts} from '../typefaces'; import {updateContentTypedefs} from '../content'; +import {updateEnumsTs} from '../enums'; import {getLanguageJSON} from '../../i18n'; @@ -137,13 +138,15 @@ const loadProject = async (projectData: IProject): Promise => { } localStorage.lastProjects = lastProjects.join(';'); + buildAssetMap(projectData); + loadScriptModels(projectData); resetTypedefs(); loadAllTypedefs(); + updateEnumsTs(); updateContentTypedefs(projectData); unloadAllEvents(); - buildAssetMap(projectData); resetPixiTextureCache(); setPixelart(projectData.settings.rendering.pixelatedrender); refreshFonts(); diff --git a/src/riotTags/catnip/catnip-block-list.tag b/src/riotTags/catnip/catnip-block-list.tag index 3f0f09d8c..8abc415e9 100644 --- a/src/riotTags/catnip/catnip-block-list.tag +++ b/src/riotTags/catnip/catnip-block-list.tag @@ -243,6 +243,7 @@ catnip-block-list( } } catch (e) { this.contextMenu.items = defaultItems; + // eslint-disable-next-line no-console console.warn(e); } this.update(); diff --git a/src/riotTags/catnip/catnip-block.tag b/src/riotTags/catnip/catnip-block.tag index 77c7968c3..b827b7bc8 100644 --- a/src/riotTags/catnip/catnip-block.tag +++ b/src/riotTags/catnip/catnip-block.tag @@ -29,11 +29,20 @@ catnip-block( svg.feather(if="{declaration && declaration.icon && !declaration.hideIcon}") use(xlink:href="#{declaration.icon}") - span.catnip-block-aTextLabel(if="{declaration && !declaration.hideLabel}" title="{(voc.blockDisplayNames[declaration.displayI18nKey] || voc.blockNames[declaration.i18nKey] || localizeField(declaration, 'displayName') || localizeField(declaration, 'name'))}") + span.catnip-block-aTextLabel( + if="{declaration && !declaration.hideLabel}" + title="{(voc.blockDisplayNames[declaration.displayI18nKey] || voc.blockNames[declaration.i18nKey] || localizeField(declaration, 'displayName') || localizeField(declaration, 'name'))}" + ) | {(voc.blockDisplayNames[declaration.displayI18nKey] || voc.blockNames[declaration.i18nKey] || localizeField(declaration, 'displayName') || localizeField(declaration, 'name'))} virtual(each="{piece in declaration.pieces}" if="{declaration && !opts.block.groupClosed}") span.catnip-block-aTextLabel(if="{piece.type === 'label'}" title="{voc.blockLabels[piece.i18nKey] || localizeField(piece, 'name')}") {voc.blockLabels[piece.i18nKey] || localizeField(piece, 'name')} span.catnip-block-aTextLabel(if="{piece.type === 'propVar'}" title="{parent.opts.block.values.variableName}") {parent.opts.block.values.variableName} + span.catnip-block-aTextLabel(if="{piece.type === 'enumValue'}" title="{getName('enum', parent.opts.block.values.enumId)}") {getName('enum', parent.opts.block.values.enumId)} + select.catnip-block-aDropdown(if="{piece.type === 'enumValue'}" onchange="{writeEnumValue}" disabled="{parent.opts.readonly}") + option( + each="{option in getEnumValues(parent.opts.block.values.enumId)}" + value="{option}" selected="{option === getValue('enumValue')}" + ) {option} svg.feather(if="{piece.type === 'icon'}") use(xlink:href="#{piece.icon}") span.catnip-block-anAsyncMarker(if="{piece.type === 'asyncMarker'}" title="{voc.asyncHint}") @@ -48,7 +57,7 @@ catnip-block( key="{piece.key}" ) textarea( - readonly="{opts.readonly}" + readonly="{parent.opts.readonly}" if="{piece.type === 'textbox'}" value="{getValue(piece.key)}" onclick="{parent.stopPropagation}" @@ -244,6 +253,7 @@ catnip-block( this.isSelected = () => isSelected(this.opts.block); const {getById, areThumbnailsIcons, getThumbnail} = require('src/node_requires/resources'); this.getName = (assetType, id) => getById(assetType, id).name; + this.getEnumValues = id => getById('enum', id).values; this.areThumbnailsIcons = areThumbnailsIcons; this.getThumbnail = (assetType, id) => getThumbnail(getById(assetType, id), false, false); this.localizeField = require('src/node_requires/i18n').localizeField; @@ -348,6 +358,9 @@ catnip-block( } this.opts.block.values[piece.key] = val; }; + this.writeEnumValue = e => { + this.opts.block.values.enumValue = e.target.value; + }; // Clicking on empty boolean fields automatically puts a constant boolean this.tryAddBoolean = e => { e.stopPropagation(); @@ -563,6 +576,7 @@ catnip-block( this.contextMenu.items = defaultMenuItems; } } catch (e) { + // eslint-disable-next-line no-console console.warn('Showing only a "Delete" option in the context menu as an error was faced while getting mutators.', e); this.contextMenu.items = [deleteMenuItem]; } diff --git a/src/riotTags/catnip/catnip-library.tag b/src/riotTags/catnip/catnip-library.tag index 620e3bb6f..0903aa7e6 100644 --- a/src/riotTags/catnip/catnip-library.tag +++ b/src/riotTags/catnip/catnip-library.tag @@ -116,7 +116,7 @@ mixin propsVars ondragend="{parent.resetTarget}" ) // Blocks for content types - br(if="{currentProject.contentTypes.length}") + h3(if="{currentProject.contentTypes.length}") {vocFull.settings.contentTypes} catnip-block( each="{contentType in currentProject.contentTypes}" if="{!opts.scriptmode}" @@ -129,6 +129,19 @@ mixin propsVars data-blockcode="content type" data-blockvalue="{contentType.name}" ) + // Blocks for Enumeration assets + h3(if="{enums.length}") {capitalize(vocGlob.assetTypes.enum[2])} + catnip-block( + each="{enum in enums}" + block="{({lib: 'core.hidden', code: 'enum value', values: {enumId: enum.uid, enumValue: enum.values[0]}})}" + dragoutonly="dragoutonly" + readonly="readonly" + ondragstart="{parent.onVarDragStart}" + draggable="draggable" + ondragend="{parent.resetTarget}" + data-blockcode="enum value" + data-blockvalue="{enum.uid}" + ) //- @attribute variables (string[]) @@ -245,9 +258,24 @@ catnip-library(class="{opts.class}").flexrow }); }); + const {getOfType, getById} = require('src/node_requires/resources'); const {blocksLibrary, startBlocksTransmit, getDeclaration, setSuggestedTarget, searchBlocks, blockFromDeclaration, emptyTexture} = require('src/node_requires/catnip'); this.categories = blocksLibrary; + this.enums = getOfType('enum'); + const updateEnums = () => { + this.enums = getOfType('enum'); + this.update(); + }; + window.signals.on('enumCreated', updateEnums); + window.signals.on('enumRemoved', updateEnums); + window.signals.on('enumChanged', updateEnums); + this.on('unmount', () => { + window.signals.off('enumCreated', updateEnums); + window.signals.off('enumRemoved', updateEnums); + window.signals.off('enumChanged', updateEnums); + }); + this.onDragStart = e => { const {block} = e.item; const declaration = getDeclaration(block.lib, block.code); @@ -271,12 +299,17 @@ catnip-library(class="{opts.class}").flexrow const bounds = e.target.getBoundingClientRect(); const code = e.currentTarget.getAttribute('data-blockcode'); const value = e.currentTarget.getAttribute('data-blockvalue'); + const values = {}; + if (code !== 'enum value') { + values.variableName = value; + } else { + values.enumId = value; + [values.enumValue] = getById('enum', value).values; + } startBlocksTransmit([{ lib: 'core.hidden', code, - values: { - variableName: value - } + values }], this.opts.blocks, false, true); window.signals.trigger( 'blockTransmissionStart', diff --git a/src/riotTags/editors/enum-editor.tag b/src/riotTags/editors/enum-editor.tag new file mode 100644 index 000000000..2d53d6d58 --- /dev/null +++ b/src/riotTags/editors/enum-editor.tag @@ -0,0 +1,72 @@ +enum-editor.aView.pad + h1 {asset.name} + ul.aStripedList + li(each="{value, ind in asset.values}") + input( + type="text" value="{value}" pattern="[a-zA-Z][a-zA-Z0-9]*" + onchange="{wire('asset.values.' + ind)}" oninput="{updateValueName}" onkeyup="{tryCreateNewValue}" + ref="inputs" + ) + code.inline {getTypescriptEnumName()}.{value} + .anActionableIcon(onclick="{copyVariantCode}" title="{vocGlob.copy}") + svg.feather + use(xlink:href="#copy") + .aSpacer.inlineblock + .anActionableIcon(onclick="{removeVariant}") + svg.feather.red + use(xlink:href="#delete") + button(onclick="{addVariant}") + svg.feather + use(xlink:href="#plus") + span {voc.addVariant} + p.dim {voc.enumUseCases} + .aSpacer + button(onclick="{applyChanges}") + svg.feather + use(xlink:href="#check") + span {vocGlob.apply} + script. + this.namespace = 'enumEditor'; + this.mixin(require('src/node_requires/riotMixins/voc').default); + this.mixin(require('src/node_requires/riotMixins/wire').default); + this.mixin(require('src/node_requires/riotMixins/discardio').default); + const {getTypescriptEnumName} = require('src/node_requires/resources/enums'); + this.getTypescriptEnumName = () => getTypescriptEnumName(this.asset); + + this.addVariant = () => { + let nameInd = 1; + while (this.asset.values.includes(`Variant${nameInd}`)) { + nameInd++; + } + this.asset.values.push(`Variant${nameInd}`); + }; + this.updateValueName = e => { + e.target.value = e.target.value.replace(/[^a-zA-Z0-9]/g, '').replace(/^[^a-zA-Z]/, ''); + }; + // Listens for Enter in the field to automatically create a new value + this.tryCreateNewValue = e => { + if (e.key === 'Enter') { + this.addVariant(); + this.update(); + const inputs = Array.isArray(this.refs.inputs) ? this.refs.inputs : [this.refs.inputs]; + const input = inputs.pop(); + input.focus(); + input.select(); + } + }; + this.copyVariantCode = e => { + const {value} = e.item; + navigator.clipboard.writeText(`${this.getTypescriptEnumName()}.${value}`); + }; + this.removeVariant = e => { + const {ind} = e.item; + this.asset.values.splice(ind, 1); + }; + this.saveAsset = () => { + this.writeChanges(); + return true; + }; + this.applyChanges = () => { + this.saveAsset(); + this.opts.ondone(this.asset); + }; diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index 640a9920f..609f57a75 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -223,6 +223,16 @@ extensions-editor selected="{parent.parent.opts.entity[ext.key] === option.value}" disabled="{option.value === ''}" ) {parent.parent.localizeField(option, 'name')} + select( + if="{ext.type.startsWith('enum@')}" + onchange="{wireAndNotify('opts.entity.'+ ext.key)}" + class="{wide: parent.opts.wide}" + ) + option( + each="{option in getEnumValues(ext.type.split('@')[1])}" + value="{option}" + selected="{parent.parent.opts.entity[ext.key] === option}" + ) {option} array-editor(if="{ext.type === 'array'}" inputtype="{ext.arrayType}" setlength="{ext.arrayLength}" entity="{parent.opts.entity[ext.key]}" compact="{parent.opts.compact}") .dim(if="{ext.help && !parent.opts.compact}") {localizeField(ext, 'help')} script. @@ -230,8 +240,13 @@ extensions-editor const fs = require('fs-extra'), path = require('path'); - this.assetTypes = require('src/node_requires/resources').assetTypes; + const {assetTypes, getById} = require('src/node_requires/resources'); + this.assetTypes = assetTypes; const {validateExtends} = require('src/node_requires/resources/content'); + this.getEnumValues = (id) => { + const {values} = getById('enum', id); + return values; + }; this.mixin(require('src/node_requires/riotMixins/wire').default); this.wireAndNotify = (...args1) => (...args2) => { diff --git a/src/styl/tags/shared/scriptables/catnip-block.styl b/src/styl/tags/shared/scriptables/catnip-block.styl index 4ccb31a8a..419388594 100644 --- a/src/styl/tags/shared/scriptables/catnip-block.styl +++ b/src/styl/tags/shared/scriptables/catnip-block.styl @@ -82,6 +82,14 @@ catnip-block, .catnip-block cursor help .catnip-block-Blocks flex 1 1 100% + .catnip-block-aDropdown + background inherit + border inherit + border-radius inherit + font inherit + padding 0.1rem 0.5rem + line-height 1 + height auto &.command > .catnip-block-aTextLabel overflow hidden line-height 1.25 From 33af0e40ebf6f632f78242d41f9df96e612c485b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 21 Jun 2024 14:59:21 +0300 Subject: [PATCH 02/44] :sparkles: New random.enumValue method --- app/data/ct.libs/random/index.js | 5 +++++ app/data/ct.libs/random/types.d.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/data/ct.libs/random/index.js b/app/data/ct.libs/random/index.js index 205f3aad8..e4c14b262 100644 --- a/app/data/ct.libs/random/index.js +++ b/app/data/ct.libs/random/index.js @@ -86,6 +86,10 @@ Object.assign(random, { } return text; }, + enumValue(en) { + const vals = Object.values(en).filter(v => Number.isFinite(v)); + return random.from(vals); + }, // Mulberry32, by bryc from https://stackoverflow.com/a/47593316 createSeededRandomizer(a) { return function seededRandomizer() { @@ -107,3 +111,4 @@ Object.assign(random, { }; random.setSeed(9323846264); } +window.random = random; diff --git a/app/data/ct.libs/random/types.d.ts b/app/data/ct.libs/random/types.d.ts index ebd56e902..083145930 100644 --- a/app/data/ct.libs/random/types.d.ts +++ b/app/data/ct.libs/random/types.d.ts @@ -64,6 +64,11 @@ declare namespace random { */ function text(text: string): string; + /** + * Returns a random value from a given enumeration. + */ + function enumValue(en: Record): number; + /** * When given both `x` and `y`, randomly returns `true` approximately `x` times * out of `y`. When given only a value between 0…100, returns `true` From e54863ea0f60066064fa6ce4c93483ddd64cb77c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 22 Jun 2024 13:24:38 +0300 Subject: [PATCH 03/44] :bug: Add a color strip to enumerations in asset browsers --- src/styl/tags/shared/asset-browser.styl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styl/tags/shared/asset-browser.styl b/src/styl/tags/shared/asset-browser.styl index 7cca38ffe..bd5b2d193 100644 --- a/src/styl/tags/shared/asset-browser.styl +++ b/src/styl/tags/shared/asset-browser.styl @@ -8,7 +8,8 @@ colorMap = { room: hue(mappedColor, 40deg), typeface: hue(mappedColor, 150deg), style: hue(mappedColor, 110deg), - behavior: hue(mappedColor, 200deg) + behavior: hue(mappedColor, 200deg), + enum: hue(mappedColor, 170deg) } for key, value in colorMap From e69d55a1a663b4a91bc9cc23d7021042b2c23264 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 23 Jun 2024 16:33:58 +0300 Subject: [PATCH 04/44] :sparkles: Map data type in content types and behaviors' fields --- app/data/i18n/Brazilian Portuguese.json | 4 +- app/data/i18n/Chinese Simplified.json | 4 +- app/data/i18n/Dutch.json | 4 +- app/data/i18n/English.json | 12 +- app/data/i18n/French.json | 4 +- app/data/i18n/German.json | 3 +- app/data/i18n/Japanese.json | 4 +- app/data/i18n/Polish.json | 3 +- app/data/i18n/Romanian.json | 3 +- app/data/i18n/Russian.json | 4 +- app/data/i18n/Spanish.json | 3 +- app/data/i18n/Turkish.json | 4 +- app/data/i18n/Ukranian.json | 4 +- src/js/projectMigrationScripts/5.0.2.js | 10 + src/node_requires/IExtensionField.d.ts | 12 +- src/node_requires/exporter/utils.ts | 79 ++--- .../resources/content/IFieldSchema.d.ts | 7 +- src/node_requires/resources/content/index.ts | 113 +++++-- src/riotTags/shared/array-editor.tag | 17 +- src/riotTags/shared/extensions-editor.tag | 3 +- src/riotTags/shared/map-editor.tag | 309 ++++++++++++++++++ 21 files changed, 515 insertions(+), 91 deletions(-) create mode 100644 src/riotTags/shared/map-editor.tag diff --git a/app/data/i18n/Brazilian Portuguese.json b/app/data/i18n/Brazilian Portuguese.json index aecf28f2f..dbfe834d2 100644 --- a/app/data/i18n/Brazilian Portuguese.json +++ b/app/data/i18n/Brazilian Portuguese.json @@ -399,7 +399,9 @@ "fieldReadableName": "Nome legível", "fieldReadableNameHint": "A versão legível do nome que será utilizado no editor de conteúdo.", "fieldType": "Tipo", - "array": "Array", + "structureTypes": { + "array": "Array" + }, "deleteContentType": "Delete esse tipo de conteúdo", "confirmDeletionMessage": "Certeza que você quer deletar esse tipo de conteúdo? Isso é irreversível e também deletará todas as entradas desse tipo de conteúdo.", "gotoEntries": "Vá para as entradas", diff --git a/app/data/i18n/Chinese Simplified.json b/app/data/i18n/Chinese Simplified.json index 21a7ea2c5..d558e0dde 100644 --- a/app/data/i18n/Chinese Simplified.json +++ b/app/data/i18n/Chinese Simplified.json @@ -407,7 +407,9 @@ "confirmDeletionMessage": "您确定要删除此内容类型吗? 它是不可逆的, 并且也将删除该内容类型的所有条目.", "gotoEntries": "进入条目", "entries": "条目", - "array": "数组", + "structureTypes": { + "array": "数组" + }, "fixedLength": "固定长度" }, "contentTypes": "内容类型", diff --git a/app/data/i18n/Dutch.json b/app/data/i18n/Dutch.json index 37d8fe29e..c985de5b4 100644 --- a/app/data/i18n/Dutch.json +++ b/app/data/i18n/Dutch.json @@ -407,7 +407,9 @@ "confirmDeletionMessage": "Ben je zeker dat je dit inhoudstype wil verwijderen? Het is onomkeerbaar en zal ook alle entries van dit inhoudstype verwijderen.", "gotoEntries": "Ga naar entries", "entries": "Entries", - "array": "Lijst", + "structureTypes": { + "array": "Lijst" + }, "fixedLength": "Vaste lengte" }, "contentTypes": "Inhoudstypes", diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index e59db9843..b56f4f1b2 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -80,7 +80,7 @@ "point2D": "2D point", "radio": "Radio buttons", "slider": "Slider", - "sliderAndNumber": "Slider with a number input", + "sliderAndNumber": "Slider with an input", "table": "Table", "text": "Short text", "textfield": "Textbox" @@ -904,7 +904,15 @@ "fieldReadableNameHint": "The readable version of the name, which is used in the content editor.", "fixedLength": "Fixed length", "fieldType": "Type", - "array": "Array", + "fieldStructure": "Structure", + "structureTypes": { + "atomic": "Single value", + "array": "Array", + "map": "Map" + }, + "key": "Key", + "value": "Value", + "mappedType": "Mapped type", "deleteContentType": "Delete this content type", "confirmDeletionMessage": "Are you sure you want to delete this content type? It is irreversible and will also delete all the entries of this content type.", "gotoEntries": "Go to entries", diff --git a/app/data/i18n/French.json b/app/data/i18n/French.json index e713bea4c..80a75bef0 100644 --- a/app/data/i18n/French.json +++ b/app/data/i18n/French.json @@ -330,7 +330,9 @@ "confirmDeletionMessage": "Êtes-vous sûr de vouloir supprimer ce type de contenu ? Ceci est irréversible et supprimera également tous les objets de ce type de contenu.", "gotoEntries": "Voir les objets", "entries": "Objets", - "array": "Liste" + "structureTypes": { + "array": "Liste" + } }, "contentTypes": "Types de contenu" }, diff --git a/app/data/i18n/German.json b/app/data/i18n/German.json index 4d8184403..fac6172ce 100644 --- a/app/data/i18n/German.json +++ b/app/data/i18n/German.json @@ -332,8 +332,7 @@ "deleteContentType": "", "confirmDeletionMessage": "", "gotoEntries": "", - "entries": "", - "array": "" + "entries": "" }, "contentTypes": "" }, diff --git a/app/data/i18n/Japanese.json b/app/data/i18n/Japanese.json index efe364323..7e42edad6 100644 --- a/app/data/i18n/Japanese.json +++ b/app/data/i18n/Japanese.json @@ -342,7 +342,9 @@ "confirmDeletionMessage": "このコンテンツタイプを本当に削除してよろしいですか?この操作は元に戻せません。", "gotoEntries": "エントリに移動", "entries": "エントリ", - "array": "配列" + "structureTypes": { + "array": "配列" + } }, "modules": { "heading": "Catmods" diff --git a/app/data/i18n/Polish.json b/app/data/i18n/Polish.json index 803bb9b8b..4c01a7ccd 100644 --- a/app/data/i18n/Polish.json +++ b/app/data/i18n/Polish.json @@ -312,8 +312,7 @@ "deleteContentType": "", "confirmDeletionMessage": "", "gotoEntries": "", - "entries": "", - "array": "" + "entries": "" }, "contentTypes": "" }, diff --git a/app/data/i18n/Romanian.json b/app/data/i18n/Romanian.json index 3254c5ba7..aa49262da 100644 --- a/app/data/i18n/Romanian.json +++ b/app/data/i18n/Romanian.json @@ -303,8 +303,7 @@ "deleteContentType": "", "confirmDeletionMessage": "", "gotoEntries": "", - "entries": "", - "array": "" + "entries": "" }, "contentTypes": "" }, diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 728d50948..fcbd21e0c 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -855,7 +855,9 @@ "confirmDeletionMessage": "Ты точно хочешь удалить этот тип контента? Это необратимо, и также удалит все записи этого типа контента.", "gotoEntries": "Перейти к записям", "entries": "Записи", - "array": "Массив", + "structureTypes": { + "array": "Массив" + }, "fixedLength": "Фиксированный размер" }, "contentTypes": "Типы контента", diff --git a/app/data/i18n/Spanish.json b/app/data/i18n/Spanish.json index 04ae4bbb0..3b7aa1fb5 100644 --- a/app/data/i18n/Spanish.json +++ b/app/data/i18n/Spanish.json @@ -303,8 +303,7 @@ "deleteContentType": "", "confirmDeletionMessage": "", "gotoEntries": "", - "entries": "", - "array": "" + "entries": "" }, "contentTypes": "" }, diff --git a/app/data/i18n/Turkish.json b/app/data/i18n/Turkish.json index d8b7a591d..2840484d2 100644 --- a/app/data/i18n/Turkish.json +++ b/app/data/i18n/Turkish.json @@ -375,7 +375,9 @@ "fieldReadableName": "Okunabilir isim", "fieldReadableNameHint": "İçerik editöründe kullanılan bu ismin okunabilir versiyonu.", "fieldType": "Tür", - "array": "Array", + "structureTypes": { + "array": "Array" + }, "deleteContentType": "Bu içerik türünü sil", "confirmDeletionMessage": "Bu içerik türünü silmek istediğinden emin misin? Bu geri alınamaz. Ayrıca bu içerik türündeki tüm girişleri de siler.", "gotoEntries": "Girişlere git", diff --git a/app/data/i18n/Ukranian.json b/app/data/i18n/Ukranian.json index e28a8ab90..f7c5ffac4 100644 --- a/app/data/i18n/Ukranian.json +++ b/app/data/i18n/Ukranian.json @@ -313,7 +313,9 @@ "confirmDeletionMessage": "Ти точно хочеш видалити цей тип контенту? Це необоротно, а також видалити всі записи цього типу контенту.", "gotoEntries": "Перейти до записів", "entries": "Записи", - "array": "Масив" + "structureTypes": { + "array": "Масив" + } }, "contentTypes": "Типи контенту" }, diff --git a/src/js/projectMigrationScripts/5.0.2.js b/src/js/projectMigrationScripts/5.0.2.js index ae7da8b85..955ff7a92 100644 --- a/src/js/projectMigrationScripts/5.0.2.js +++ b/src/js/projectMigrationScripts/5.0.2.js @@ -5,6 +5,16 @@ window.migrationProcess.push({ process: project => new Promise(resolve => { // Enum asset type appeared project.settings.export.bundleAssetTypes ??= false; + // Content types' fields can now choose between atomic values, arrays, and maps + for (const contentType of project.contentTypes) { + for (const field of contentType.specification) { + if (!('structure' in field)) { + field.structure = field.array ? 'array' : 'atomic'; + field.mappedType = 'text'; + delete field.array; + } + } + } resolve(); }) }); diff --git a/src/node_requires/IExtensionField.d.ts b/src/node_requires/IExtensionField.d.ts index 97113d80c..6e13890a9 100644 --- a/src/node_requires/IExtensionField.d.ts +++ b/src/node_requires/IExtensionField.d.ts @@ -9,7 +9,7 @@ declare interface IExtensionField { 'text' | 'textfield' | 'code' | 'number' | 'slider' | 'sliderAndNumber' | 'point2D' | 'color' | 'checkbox' | 'radio' | 'select' | 'icon' | - 'group' | 'table' | 'array' | + 'group' | 'table' | 'array' | 'map' | resourceType, /** * The name of a JSON key to write into the `opts.entity`. @@ -52,7 +52,15 @@ declare interface IExtensionField { * It supports a subset of fields supported by extensions-editor itself, * excluding headers, groups, tables, icons, radio, select, and arrays. */ - arrayType?: string, + arrayType?: Exclude, + /** + * Determines the type of field used for key inputs of fields with type === 'map' + */ + mapKeyType?: Exclude, + /** + * Determines the type of field used for values of fields with type === 'map' + */ + mapValueType?: IExtensionField['arrayType'], /** * If set and used with type=array, presets the amount of values in the created * arrays. Values cannot be removed, and new ones can't be added. diff --git a/src/node_requires/exporter/utils.ts b/src/node_requires/exporter/utils.ts index fb25457a2..a1072ddde 100644 --- a/src/node_requires/exporter/utils.ts +++ b/src/node_requires/exporter/utils.ts @@ -53,6 +53,29 @@ export const getUnwrappedExtends = (exts: Record): Record['arrayType']) => { + if (assetTypes.includes(fieldType as resourceType)) { + if (value === -1) { + return -1; + } + try { + const asset = getById(fieldType, value as string); + return asset.name; + } catch (e) { + alertify.error(`Could not resolve UID ${value} as a ${fieldType}. Returning -1.`); + console.error(e); + // eslint-disable-next-line no-console + console.trace(); + return -1; + } + } + if (fieldType.startsWith('enum@')) { + const {values} = getById('enum', fieldType.split('@').pop()!); + return values.indexOf(value as string); + } + return value; +}; /** * Supports flat objects only. * A helper for a content function; unwraps IDs for assets @@ -63,7 +86,7 @@ export const getUnwrappedExtends = (exts: Record): Record, + exts: Record, spec: IContentType['specification'] ): Record => { const fieldMap = {} as Record; @@ -77,50 +100,20 @@ export const getUnwrappedBySpec = ( out[i] = exts[i]; continue; } - if ((unwrappable.includes(fieldMap[i].type)) && - (exts[i] === void 0 || exts[i] === -1)) { - // Skip unset values - continue; - } - // Turn enum entries' names into numerical constant equivalent to this enum's value. - if (fieldMap[i].type.startsWith('enum@')) { - const [, id] = fieldMap[i].type.split('@'); - const enumAsset = getById('enum', id); - if (fieldMap[i].array) { - out[i] = (exts[i] as string[]).map(elt => enumAsset.values.indexOf(elt)); - } else { - out[i] = enumAsset.values.indexOf(exts[i] as string); - } - continue; - } - if (unwrappable.includes(fieldMap[i].type)) { - if (fieldMap[i].array) { - out[i] = (exts[i] as string[]).map(elt => { - try { - const asset = getById(fieldMap[i].type, elt); - return asset.name; - } catch (e) { - alertify.error(`Could not resolve UID ${elt} for field ${i} as a ${fieldMap[i].type}. Returning -1. Full object: ${JSON.stringify(exts)}`); - console.error(e); - // eslint-disable-next-line no-console - console.trace(); - return -1; - } - }); - continue; - } - try { - out[i] = getById(fieldMap[i].type as resourceType, String(exts[i])).name; - } catch (e) { - alertify.error(`Could not resolve UID ${exts[i]} for field ${i} as a ${fieldMap[i].type}. Returning -1. Full object: ${JSON.stringify(exts)}`); - console.error(e); - // eslint-disable-next-line no-console - console.trace(); - out[i] = -1; + const field = fieldMap[i]; + if (field.structure === 'array') { + out[i] = (exts[i] as (string | number)[]) + .map(elt => getUnreferencedValue(elt, field.type)); + } else if (field.structure === 'map') { + out[i] = {}; + const inMap = exts[i] as Record; + const outMap = out[i] as Record; + for (const key of Object.keys(inMap)) { + outMap[getUnreferencedValue(key, field.type)] = + getUnreferencedValue(inMap[key], field.mappedType!); } } else { - // Seems to be a plain value. Output the old key as is. - out[i] = exts[i]; + out[i] = getUnreferencedValue(exts[i] as string | number, field.type); } } return out; diff --git a/src/node_requires/resources/content/IFieldSchema.d.ts b/src/node_requires/resources/content/IFieldSchema.d.ts index d79f53e45..af5735068 100644 --- a/src/node_requires/resources/content/IFieldSchema.d.ts +++ b/src/node_requires/resources/content/IFieldSchema.d.ts @@ -3,8 +3,9 @@ type enumId = `enum@${id}`; declare interface IFieldSchema { name: string, readableName: string, - type: resourceType | 'text' | 'textfield' | 'code' | 'number' | 'sliderAndNumber' | 'point2D' | 'checkbox' | 'color' | 'icon', - required: boolean - array: boolean, + type: resourceType | 'text' | 'textfield' | 'code' | 'number' | 'sliderAndNumber' | 'checkbox' | 'color', + required: boolean, + structure: 'atomic' | 'array' | 'map', + mappedType?: IFieldSchema['type'], fixedLength?: number } diff --git a/src/node_requires/resources/content/index.ts b/src/node_requires/resources/content/index.ts index fbb0bbf51..7859bddd7 100644 --- a/src/node_requires/resources/content/index.ts +++ b/src/node_requires/resources/content/index.ts @@ -6,7 +6,7 @@ const capitalize = (str: string): string => str.slice(0, 1).toUpperCase() + str. export const getFieldsExtends = (): IExtensionField[] => { const enums = getOfType('enum'); - const defaultFieldTypes = ['text', 'textfield', 'code', '', 'number', 'sliderAndNumber', 'point2D', '', ...assetTypes, '', 'checkbox', 'color']; + const defaultFieldTypes = ['text', 'textfield', 'code', '', 'number', 'sliderAndNumber', '', ...assetTypes, '', 'checkbox', 'color']; if (getOfType('enum').length) { defaultFieldTypes.push(''); } @@ -38,6 +38,26 @@ export const getFieldsExtends = (): IExtensionField[] => { type: 'text', key: 'readableName', help: getByPath('settings.content.fieldReadableNameHint') + }, { + name: getByPath('common.required'), + type: 'checkbox', + key: 'required', + default: false + }, { + name: getByPath('settings.content.fieldStructure'), + type: 'select', + key: 'structure', + default: 'atomic', + options: [{ + name: getByPath('settings.content.structureTypes.atomic'), + value: 'atomic' + }, { + name: getByPath('settings.content.structureTypes.array'), + value: 'array' + }, { + name: getByPath('settings.content.structureTypes.map'), + value: 'map' + }] }, { name: getByPath('settings.content.fieldType'), type: 'select', @@ -45,25 +65,23 @@ export const getFieldsExtends = (): IExtensionField[] => { options: fieldTypeOptions, default: 'text' }, { - name: getByPath('common.required'), - type: 'checkbox', - key: 'required', - default: false - }, { - name: getByPath('settings.content.array'), - type: 'checkbox', - key: 'array', - default: false + name: getByPath('settings.content.mappedType'), + type: 'select', + key: 'mappedType', + options: fieldTypeOptions, + if: ['structure', 'map'], + default: 'number' }, { name: getByPath('settings.content.fixedLength'), type: 'number', key: 'fixedLength', - if: 'array' + if: ['structure', 'array'] }] as IExtensionField[] }]; return options; }; +/** Returns an object for extensions-editor to display UI for configuring content types */ export const getExtends = (): IExtensionField[] => [{ name: getByPath('settings.content.typeName') as string, type: 'text', @@ -87,9 +105,7 @@ export const fieldTypeToTsType: Record = { code: 'string', color: 'string', number: 'number', - point2D: '[number, number]', sliderAndNumber: 'number', - icon: 'string', text: 'string', room: 'string', sound: 'string', @@ -104,16 +120,25 @@ export const fieldTypeToTsType: Record = { enum: 'string' }; -const getFieldsTsType = (field: IFieldSchema): string => { - if (!field.type.startsWith('enum@')) { - return fieldTypeToTsType[field.type]; +const getTsTypeFromFieldType = (ftype: IFieldSchema['type']): string => { + if (!ftype.startsWith('enum@')) { + return fieldTypeToTsType[ftype]; + } + return getTypescriptEnumName(getById('enum', ftype.split('@')[1])); +}; +const getTsFieldType = (field: IContentType['specification'][0]) => { + if (field.structure === 'array') { + return `${getTsTypeFromFieldType(field.type)}[]`; + } + if (field.structure === 'map') { + return `Record<${getTsTypeFromFieldType(field.type)}, ${getTsTypeFromFieldType(field.mappedType!)}>`; } - return getTypescriptEnumName(getById('enum', field.type.split('@')[1])); + return getTsTypeFromFieldType(field.type); }; const getTsType = (content: IContentType): string => { const fields = content.specification .map(f => ` /**${f.readableName || f.name}*/ - '${f.name}': ${getFieldsTsType(f)}${f.array ? '[]' : ''};`) + '${f.name}': ${getTsFieldType(f)};`) .join('\n'); return ` var ${content.name}: { @@ -142,18 +167,28 @@ export const updateContentTypedefs = (project: IProject) => { export const schemaToExtensions = (schema: IFieldSchema[]): IExtensionField[] => schema .map((spec: IFieldSchema) => { + let fieldType: IExtensionField['type'] = spec.type || 'text'; + if (spec.structure === 'array') { + fieldType = 'array'; + } else if (spec.structure === 'map') { + fieldType = 'map'; + } const field: IExtensionField = { key: spec.name || spec.readableName, name: spec.readableName || spec.name, - type: spec.array ? 'array' : (spec.type || 'text'), + type: fieldType, required: spec.required }; - if (field.type === 'array') { - field.arrayType = spec.type || 'text'; + if (spec.structure === 'array') { + field.arrayType = spec.type || ('text' as IExtensionField['type']); if (spec.fixedLength) { field.arrayLength = spec.fixedLength; } field.default = () => [] as unknown[]; + } else if (spec.structure === 'map') { + field.mapKeyType = spec.type; + field.mapValueType = spec.mappedType; + field.default = () => ({} as Record); } else if (field.type === 'sliderAndNumber') { field.min = 0; field.max = 100; @@ -179,7 +214,7 @@ const validateRef = (val: unknown, assetType: resourceType): boolean => { return exists(assetType, val); }; -type directlyValidated = Exclude; +type directlyValidated = Exclude; // For each field type, map it to a tuple of a validation function and a default value getter. const validationTypeMap: Record enumAsset.values[0] ]; +const isValid = (value: any, fieldType: directlyValidated | `enum@${string}`): boolean => { + if (fieldType.startsWith('enum@')) { + const [, id] = fieldType.split('@'); + const enumAsset = getById('enum', id); + return enumValidatorTuple[0](value, enumAsset); + } + const [validator] = validationTypeMap[fieldType as directlyValidated]; + return validator(value); +}; /** * Checks a primitive value against its type and resets it to its default value if it is invalid. */ @@ -258,7 +302,7 @@ export const validateContentEntries = ( for (const field of schema) { let val = target[field.name]; const ftype = field.type; - if (field.array) { + if (field.structure === 'array') { if (!Array.isArray(val)) { val = target[field.name] = []; } @@ -266,6 +310,16 @@ export const validateContentEntries = ( for (let i = 0; i < elts.length; i++) { validateValue(elts, i, ftype); } + } else if (field.structure === 'map') { + const map = val as Record; + for (const key in map) { + // Remove invalid keys + if (!isValid(key, field.type)) { + delete map[key]; + continue; + } + validateValue(val, key, field.mappedType!); + } } else { validateValue(target, field.name, ftype); } @@ -290,6 +344,19 @@ export const validateExtends = ( for (let i = 0; i < (target[extension.key] as unknown[]).length; i++) { validateValue(target[extension.key], i, extension.arrayType! as directlyValidated); } + } else if (extension.type === 'map') { + if (typeof target[extension.key] !== 'object' || Array.isArray(target[extension.key])) { + target[extension.key] = {}; + } + const map = target[extension.key] as Record; + for (const key of Object.keys(map)) { + // Remove invalid keys + if (!isValid(key, extension.mapKeyType!)) { + delete map[key]; + continue; + } + validateValue(map, key, extension.mapValueType!); + } } else if (extension.type === 'group' && extension.items) { if (typeof target[extension.key] !== 'object' || Array.isArray(target[extension.key])) { target[extension.key] = {}; diff --git a/src/riotTags/shared/array-editor.tag b/src/riotTags/shared/array-editor.tag index fbfdd78e5..e75f33d39 100644 --- a/src/riotTags/shared/array-editor.tag +++ b/src/riotTags/shared/array-editor.tag @@ -33,6 +33,16 @@ array-editor assetid="{item}" onchanged="{parent.writeUid(index)}" ) + select( + if="{parent.opts.inputtype.startsWith('enum@')}" + onchange="{wireAndNotify('opts.entity.'+ ext.key)}" + class="{wide: parent.opts.wide}" + ) + option( + each="{option in getEnumValues(parent.opts.inputtype.split('@')[1])}" + value="{option}" + selected="{parent.parent.opts.entity[ext.key] === option}" + ) {option} .aPoint2DInput(if="{parent.opts.inputtype === 'point2D'}") label span X: @@ -121,7 +131,12 @@ array-editor use(xlink:href="#plus") span {voc.addRow} script. - this.assetTypes = require('src/node_requires/resources').assetTypes; + const {assetTypes, getById} = require('src/node_requires/resources'); + this.assetTypes = assetTypes; + this.getEnumValues = (id) => { + const {values} = getById('enum', id); + return values; + }; this.mixin(require('src/node_requires/riotMixins/wire').default); this.wireAndNotify = (...args1) => (...args2) => { diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index 609f57a75..30bdaef4d 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -23,7 +23,7 @@ Extensions are an array of IExtensionField objects (Type definitions in node_requires). extensions-editor - virtual(each="{ext in extensions}" if="{!ext.if || opts.entity[ext.if]}") + virtual(each="{ext in extensions}" if="{!ext.if || (Array.isArray(ext.if) ? opts.entity[ext.if[0]] === ext.if[1] : opts.entity[ext.if])}") // ext="{ext}" is a workaround to lost loop variables in yields collapsible-section.aPanel( ext="{ext}" @@ -234,6 +234,7 @@ extensions-editor selected="{parent.parent.opts.entity[ext.key] === option}" ) {option} array-editor(if="{ext.type === 'array'}" inputtype="{ext.arrayType}" setlength="{ext.arrayLength}" entity="{parent.opts.entity[ext.key]}" compact="{parent.opts.compact}") + map-editor(if="{ext.type === 'map'}" keytype="{ext.mapKeyType}" valuetype="{ext.mapValueType}" entity="{parent.opts.entity[ext.key]}" compact="{parent.opts.compact}") .dim(if="{ext.help && !parent.opts.compact}") {localizeField(ext, 'help')} script. const libsDir = './data/ct.libs'; diff --git a/src/riotTags/shared/map-editor.tag b/src/riotTags/shared/map-editor.tag new file mode 100644 index 000000000..f5f47b222 --- /dev/null +++ b/src/riotTags/shared/map-editor.tag @@ -0,0 +1,309 @@ +// + This tag allows editing a map, where key and value types can be of different type. + + @attribute entity (riot object) + The entity to edit + @attribute keytype (string) + The input type used for keys. + One of the types supported by extensions-editor, except for headers, arrays, maps, groups and tables. + @attribute valuetype (string) + The input type used for keys. + One of the types supported by extensions-editor, except for headers, arrays, maps, groups and tables. + @attribute [onchanged] (riot Function) + A callback to call when the entity has changed. + @attribute [wide] (atomic) + @attribute [compact] (atomic) + +map-editor + table.aNiceTable.nmt(class="{dense: opts.compact}") + tr + th {vocFull.settings.content.key} + th {vocFull.settings.content.value} + th + tr(if="{!Object.keys(opts.entity).length}") + td(colspan="3") {voc.noEntries} + tr(each="{value, key in opts.entity}" no-reorder) + td + input.nogrow( + if="{parent.opts.keytype === 'checkbox'}" + type="checkbox" + checked="{key}" + onchange="{parent.changeKey}" + ) + asset-input( + if="{assetTypes.includes(parent.opts.keytype)}" + assettypes="{parent.opts.keytype}" + allowclear="yep" + compact="compact" + assetid="{key}" + onchanged="{parent.changeKeyUid(key)}" + ) + select( + if="{parent.opts.keytype.startsWith('enum@')}" + onchange="{parent.changeKey}" + class="{wide: parent.opts.wide}" + value="{key}" + ) + option( + each="{option in getEnumValues(parent.opts.keytype.split('@')[1])}" + value="{option}" + selected="{key === option}" + ) {option} + color-input( + if="{parent.opts.keytype === 'color'}" + class="{wide: parent.opts.wide}" + color="{key}" + hidealpha="{ext.noalpha ? 'noalpha' : ''}" + onapply="{parent.changeKey}" + ) + input( + if="{parent.opts.keytype === 'text'}" + class="{wide: parent.opts.wide}" + type="text" + value="{key}" + onchange="{parent.changeKey}" + ) + textarea( + if="{parent.opts.keytype === 'textfield'}" + class="{wide: parent.opts.wide}" + value="{key}" + onchange="{parent.changeKey}" + ) + textarea.monospace( + if="{parent.opts.keytype === 'code'}" + class="{wide: parent.opts.wide}" + value="{key}" + onchange="{parent.changeKey}" + ) + input( + if="{parent.opts.keytype === 'number'}" + class="{wide: parent.opts.wide}" + type="number" + value="{key}" + onchange="{parent.changeKey}" + ) + .aSliderWrap(if="{parent.opts.keytype === 'slider'}") + input( + class="{wide: parent.opts.wide}" + type="range" + value="{key}" + onchange="{parent.changeKey}" + min="0" max="100" step="0.1" + ) + .flexrow(if="{parent.opts.keytype === 'sliderAndNumber'}") + .aSliderWrap + input( + class="{compact: parent.opts.compact}" + type="range" + value="{key}" + onchange="{parent.changeKey}" + min="0" max="100" step="0.1" + ) + .aSpacer + input( + class="{compact: parent.opts.compact, invalid: ext.required && !Number.isFinite(parent.opts.entity[ext.key])}" + type="number" + value="{key}" + onchange="{parent.changeKey}" + min="0" max="100" step="0.1" + ) + td + input.nogrow( + if="{parent.opts.valuetype === 'checkbox'}" + type="checkbox" + checked="{value}" + onchange="{parent.changeValue}" + ) + asset-input( + if="{assetTypes.includes(parent.opts.valuetype)}" + assettypes="{parent.opts.valuetype}" + allowclear="yep" + compact="compact" + assetid="{value}" + onchanged="{parent.changeValueUid(key)}" + ) + select( + if="{parent.opts.valuetype.startsWith('enum@')}" + onchange="{parent.changeValue}" + class="{wide: parent.opts.wide}" + ) + option( + each="{option in getEnumValues(parent.opts.valuetype.split('@')[1])}" + value="{option}" + selected="{key === option}" + ) {option} + color-input( + if="{parent.opts.valuetype === 'color'}" + class="{wide: parent.opts.wide}" + color="{value}" + hidealpha="{ext.noalpha ? 'noalpha' : ''}" + onapply="{parent.changeValue}" + ) + input( + if="{parent.opts.valuetype === 'text'}" + class="{wide: parent.opts.wide}" + type="text" + value="{value}" + onchange="{parent.changeValue}" + ) + textarea( + if="{parent.opts.valuetype === 'textfield'}" + class="{wide: parent.opts.wide}" + value="{value}" + onchange="{parent.changeValue}" + ) + textarea.monospace( + if="{parent.opts.valuetype === 'code'}" + class="{wide: parent.opts.wide}" + value="{value}" + onchange="{parent.changeValue}" + ) + input( + if="{parent.opts.valuetype === 'number'}" + class="{wide: parent.opts.wide}" + type="number" + value="{value}" + onchange="{parent.changeValue}" + ) + .aSliderWrap(if="{parent.opts.valuetype === 'slider'}") + input( + class="{wide: parent.opts.wide}" + type="range" + value="{value}" + onchange="{parent.changeValue}" + min="0" max="100" step="0.1" + ) + .flexrow(if="{parent.opts.valuetype === 'sliderAndNumber'}") + .aSliderWrap + input( + class="{compact: parent.opts.compact}" + type="range" + value="{value}" + onchange="{parent.changeValue}" + min="0" max="100" step="0.1" + ) + .aSpacer + input( + class="{compact: parent.opts.compact, invalid: ext.required && !Number.isFinite(parent.opts.entity[ext.key])}" + type="number" + value="{value}" + onchange="{parent.changeValue}" + min="0" max="100" step="0.1" + ) + td + .anActionableIcon(title="{vocGlob.delete}" onclick="{deleteKey}") + svg.feather.red + use(xlink:href="#delete") + button(onclick="{addRow}" class="{inline: opts.compact}") + svg.feather + use(xlink:href="#plus") + span {voc.addRow} + script. + const {assetTypes, getById} = require('src/node_requires/resources'); + this.assetTypes = assetTypes; + this.getEnumValues = (id) => { + const {values} = getById('enum', id); + return values; + }; + + this.mixin(require('src/node_requires/riotMixins/wire').default); + this.wireAndNotify = (...args1) => (...args2) => { + this.wire(...args1)(...args2); + if (this.opts.onchanged) { + this.opts.onchanged(); + } + }; + this.namespace = 'extensionsEditor'; + this.mixin(require('src/node_requires/riotMixins/voc').default); + + if (this.opts.setlength) { + this.opts.entity.length = Number(this.opts.setlength); + } + + this.on('update', () => { + if (!this.opts.entity) { + console.error('array-editor tag did not receive its `entity` object for editing!'); + // eslint-disable-next-line no-console + console.warn(this); + } + }); + + const getValue = input => { + if (input instanceof HTMLInputElement && input.type === 'checkbox') { + return input.checked; + } else if (input instanceof HTMLInputElement && (input.type === 'number' || input.type === 'range')) { + let val = Number(input.value); + if (input.hasAttribute('data-wired-force-minmax')) { + val = Math.max(Number(input.min), Math.min(Number(input.max), val)); + } + return val; + } + return input.value; + }; + this.changeKey = e => { + const {key, value} = e.item; + delete this.opts.entity[key]; + this.opts.entity[getValue(e.target)] = value; + }; + this.changeKeyUid = key => uid => { + const value = this.opts.entity[key]; + delete this.opts.entity[key]; + this.opts.entity[uid || -1] = value; + this.update(); + }; + this.changeValue = e => { + const {key} = e.item; + this.opts.entity[key] = getValue(e.target); + }; + this.changeValueUid = key => uid => { + this.opts.entity[key] = uid || -1; + this.update(); + }; + + this.addRow = () => { + let key, value; + if ([assetTypes].includes(this.opts.valuetype)) { + value = -1; + } else if (this.opts.valuetype === 'checkbox') { + value = false; + } else if (['number', 'sliderAndNumber', 'slider'].includes(this.opts.valuetype)) { + value = 0; + } else if (this.opts.valuetype.startsWith('enum@')) { + const [, uid] = this.opts.valuetype.split('@'); + [value] = getById('enum', uid).values; + } else { + value = ''; + } + + if ([assetTypes].includes(this.opts.keytype) && !(-1 in this.opts.entity)) { + key = -1; + } else if (this.opts.keytype === 'checkbox' && !this.opts.entity.false) { // Why would someone ever use it? + key = false; + } else if (['number', 'sliderAndNumber', 'slider'].includes(this.opts.keytype)) { + key = 0; + while (key in this.opts.entity) { + key++; + } + } else if (this.opts.keytype.startsWith('enum@')) { + const [, uid] = this.opts.keytype.split('@'); + let i = 0; + const {values} = getById('enum', uid); + while (i < values.length && (values[i] in this.opts.entity)) { + i++; + } + key = values[i]; + } else { + let i = 0; + while (`key${i}` in this.opts.entity) { + i++; + } + key = `key${i}`; + } + if (key !== void 0) { + this.opts.entity[key] = value; + } + }; + this.deleteKey = e => { + const {key} = e.item; + delete this.opts.entity[key]; + }; From c34d0868a62670d2f98292456a8c5e9fa604cbd9 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 23 Jun 2024 16:41:26 +0300 Subject: [PATCH 05/44] :bug: Fix enum fields having `undefined` type in events. --- src/node_requires/events/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/node_requires/events/index.ts b/src/node_requires/events/index.ts index 7b296b480..25d9cae63 100644 --- a/src/node_requires/events/index.ts +++ b/src/node_requires/events/index.ts @@ -1,6 +1,7 @@ import {getLanguageJSON, localizeField} from '../i18n'; import {assetTypes, getById, getThumbnail} from '../resources'; import {fieldTypeToTsType} from '../resources/content'; +import {getTypescriptEnumName} from '../resources/enums'; const categories: Record = { lifecycle: { @@ -266,7 +267,13 @@ export const getFieldsTypeScript = (asset: IScriptable | IScriptableBehaviors): if (behavior.specification.length) { code += '&{'; for (const field of behavior.specification) { - code += `${field.name || field.readableName}: ${fieldTypeToTsType[field.type]};`; + // eslint-disable-next-line max-depth + if (field.type.startsWith('enum@')) { + const en = getById('enum', field.type.split('@')[1]); + code += `${field.name || field.readableName}: ${getTypescriptEnumName(en)};`; + } else { + code += `${field.name || field.readableName}: ${fieldTypeToTsType[field.type]};`; + } } code += behavior.extendTypes.split('\n').join(''); code += '}'; @@ -277,7 +284,12 @@ export const getFieldsTypeScript = (asset: IScriptable | IScriptableBehaviors): const behavior = asset as IBehavior; code += '&{'; for (const field of behavior.specification) { - code += `${field.name || field.readableName}: ${fieldTypeToTsType[field.type]};`; + if (field.type.startsWith('enum@')) { + const en = getById('enum', field.type.split('@')[1]); + code += `${field.name || field.readableName}: ${getTypescriptEnumName(en)};`; + } else { + code += `${field.name || field.readableName}: ${fieldTypeToTsType[field.type]};`; + } } code += behavior.extendTypes.split('\n').join(''); code += '}'; From 7eba0c9a832e47c27e103c8b408869cf416a2657 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 23 Jun 2024 16:48:18 +0300 Subject: [PATCH 06/44] :bug: Properly remove deleted enumerations from fields that use it as an input type --- src/node_requires/resources/enums/index.ts | 18 ++++++++++++++++++ src/node_requires/resources/index.ts | 2 +- src/riotTags/editors/behavior-editor.tag | 13 +++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/node_requires/resources/enums/index.ts b/src/node_requires/resources/enums/index.ts index 00efec7c4..a535b83af 100644 --- a/src/node_requires/resources/enums/index.ts +++ b/src/node_requires/resources/enums/index.ts @@ -39,3 +39,21 @@ export const createAsset = async (): Promise => { }; return enumAsset; }; + +export const removeAsset = (en: IEnum) => { + // Remove enumeration as input type from all content types and behaviors. + for (const contentType of currentProject.contentTypes) { + for (const field of contentType.specification) { + if (field.type === `enum@${en.uid}`) { + field.type = 'text'; + } + } + } + for (const behavior of getOfType('behavior')) { + for (const field of behavior.specification) { + if (field.type === `enum@${en.uid}`) { + field.type = 'text'; + } + } + } +}; diff --git a/src/node_requires/resources/index.ts b/src/node_requires/resources/index.ts index acaaeff65..ff92725e9 100644 --- a/src/node_requires/resources/index.ts +++ b/src/node_requires/resources/index.ts @@ -89,7 +89,7 @@ type typeToTsTypeMap = { T extends 'tandem' ? ITandem : T extends 'template' ? ITemplate : T extends 'behavior' ? IBehavior : - T extends 'enum'? IEnum : + T extends 'enum' ? IEnum : T extends 'script' ? IScript : never; } diff --git a/src/riotTags/editors/behavior-editor.tag b/src/riotTags/editors/behavior-editor.tag index fe0747c6a..250446a05 100644 --- a/src/riotTags/editors/behavior-editor.tag +++ b/src/riotTags/editors/behavior-editor.tag @@ -53,6 +53,19 @@ behavior-editor.aPanel.aView.flexrow this.currentSheet = 'fields'; }; + const cleanupEnums = en => { + for (const field of this.asset.specification) { + if (field.type === `enum@${en}`) { + field.type = 'text'; + } + } + this.update(); + }; + window.signals.on('enumRemoved', cleanupEnums); + this.on('unmount', () => { + window.signals.off('enumRemoved', cleanupEnums); + }); + this.saveAsset = () => { this.writeChanges(); return true; From 77f2870d967aef3208280ea8980184a73689ef02 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Mon, 24 Jun 2024 17:25:46 +0300 Subject: [PATCH 07/44] :zap: Internal: Improve how ct.js exposes base classes and Room to code editors --- src/ct.release/scripts.ts | 2 +- src/ct.release/templateBaseClasses/index.ts | 5 +++ src/js/utils/codeEditorHelpers.js | 41 +++++++++++++------ src/node_requires/events/index.ts | 4 -- src/riotTags/editors/script-editor.tag | 5 +-- .../scriptables/code-editor-scriptable.tag | 7 ++-- 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/ct.release/scripts.ts b/src/ct.release/scripts.ts index 78e1b906c..f23940da7 100644 --- a/src/ct.release/scripts.ts +++ b/src/ct.release/scripts.ts @@ -1,4 +1,4 @@ /* eslint-disable object-curly-newline */ -export const scriptsLib: Record any> = { +export const scriptsLib: Record any> = { /*!@scriptAssets@*/ }; diff --git a/src/ct.release/templateBaseClasses/index.ts b/src/ct.release/templateBaseClasses/index.ts index f61bb2236..dad44e658 100644 --- a/src/ct.release/templateBaseClasses/index.ts +++ b/src/ct.release/templateBaseClasses/index.ts @@ -47,6 +47,11 @@ export type CopyPanel = Record & PixiPanel & ICopy; * It has functionality of both PIXI.Text and ct.js Copies. */ export type CopyText = Record & PixiText & ICopy; +/** + * An instance of a ct.js template with BitmapText as its base class. + * It has functionality of both PIXI.BitmapText and ct.js Copies. + */ +export type CopyBitmapText = Record & PixiBitmapText & ICopy; /** * An instance of a ct.js template with Container as its base class. * It has functionality of both PIXI.Container and ct.js Copies, and though by itself it doesn't diff --git a/src/js/utils/codeEditorHelpers.js b/src/js/utils/codeEditorHelpers.js index f5dea1b74..ee073264d 100644 --- a/src/js/utils/codeEditorHelpers.js +++ b/src/js/utils/codeEditorHelpers.js @@ -42,11 +42,23 @@ pixiDtsPromise, globalsPromise ]); - const exposer = ` + + // Republish pixi.js and ct.js objects in global space + const exposePixiModule = ` declare module 'pixi.js' { export * from 'bundles/pixi.js/src/index'; }`; - const publiciser = ` + + const {baseClassToTS} = require('src/node_requires/resources/templates'); + const baseClassesImports = ` + import {${Object.values(baseClassToTS).map(bc => `${bc} as ${bc}Temp`) + .join(', ')}} from 'src/ct.release/templateBaseClasses/index'; + `; + const baseClassesGlobals = Object.values(baseClassToTS) + .map(bc => `type ${bc} = ${bc}Temp;`) + .join('\n'); + + const exposeCtJsModules = ` import * as pixiTemp from 'bundles/pixi.js/src/index'; import {actionsLib as actionsTemp, inputsLib as inputsTemp} from 'src/ct.release/inputs'; import backgroundsTemp from 'src/ct.release/backgrounds'; @@ -55,19 +67,19 @@ import emittersTemp from 'src/ct.release/emitters'; import resTemp from 'src/ct.release/res'; import roomsTemp, {Room as roomClass} from 'src/ct.release/rooms'; - import scriptsTemp from 'src/ct.release/scripts'; + import {scriptsLib as scriptsTemp} from 'src/ct.release/scripts'; import soundsTemp from 'src/ct.release/sounds'; import stylesTemp from 'src/ct.release/styles'; - import templatesTemp from 'src/ct.release/templates'; + import templatesTemp, {BasicCopy as BasicCopyTemp} from 'src/ct.release/templates'; + ${baseClassesImports} import tilemapsTemp from 'src/ct.release/tilemaps'; import timerTemp from 'src/ct.release/timer'; import uTemp from 'src/ct.release/u'; import behaviorsTemp from 'src/ct.release/behaviors'; import {meta as metaTemp, settings as settingsTemp, pixiApp as pixiAppTemp} from 'src/ct.release/index'; declare global { - ${globalsDts} var PIXI: typeof pixiTemp; - var Room: typeof roomClass; + type Room = roomClass; var actions: typeof actionsTemp; var backgrounds: typeof backgroundsTemp; var behaviors: typeof behaviorsTemp; @@ -87,16 +99,21 @@ var timer: typeof timerTemp; var u: typeof uTemp; + type BasicCopy = BasicCopyTemp; + ${baseClassesGlobals} + var pixiApp: typeof pixiAppTemp; }`; - ts.javascriptDefaults.addExtraLib(ctDts, monaco.Uri.parse('file:///ctjs.ts')); - ts.typescriptDefaults.addExtraLib(ctDts, monaco.Uri.parse('file:///ctjs.ts')); + ts.javascriptDefaults.addExtraLib(ctDts, monaco.Uri.parse('file:///ctjs.d.ts')); + ts.typescriptDefaults.addExtraLib(ctDts, monaco.Uri.parse('file:///ctjs.d.ts')); ts.javascriptDefaults.addExtraLib(pixiDts, monaco.Uri.parse('file:///pixi.ts')); ts.typescriptDefaults.addExtraLib(pixiDts, monaco.Uri.parse('file:///pixi.ts')); - ts.javascriptDefaults.addExtraLib(exposer, monaco.Uri.parse('file:///exposer.ts')); - ts.typescriptDefaults.addExtraLib(exposer, monaco.Uri.parse('file:///exposer.ts')); - ts.javascriptDefaults.addExtraLib(publiciser, monaco.Uri.parse('file:///publiciser.ts')); - ts.typescriptDefaults.addExtraLib(publiciser, monaco.Uri.parse('file:///publiciser.ts')); + ts.javascriptDefaults.addExtraLib(exposePixiModule, monaco.Uri.parse('file:///piximodule.d.ts')); + ts.typescriptDefaults.addExtraLib(exposePixiModule, monaco.Uri.parse('file:///piximodule.d.ts')); + ts.javascriptDefaults.addExtraLib(exposeCtJsModules, monaco.Uri.parse('file:///ctjsModules.d.ts')); + ts.typescriptDefaults.addExtraLib(exposeCtJsModules, monaco.Uri.parse('file:///ctjsModules.d.ts')); + ts.javascriptDefaults.addExtraLib(globalsDts, monaco.Uri.parse('file:///globals.d.ts')); + ts.typescriptDefaults.addExtraLib(globalsDts, monaco.Uri.parse('file:///globals.d.ts')); }); /** diff --git a/src/node_requires/events/index.ts b/src/node_requires/events/index.ts index 25d9cae63..ded07fb3e 100644 --- a/src/node_requires/events/index.ts +++ b/src/node_requires/events/index.ts @@ -324,9 +324,6 @@ export const getBehaviorFields = (asset: IScriptable | IScriptableBehaviors): st return fields; }; -import {baseClassToTS} from '../resources/templates'; -const baseTypes = `import {BasicCopy} from 'src/ct.release/templates';import {${Object.values(baseClassToTS).join(', ')}} from 'src/ct.release/templateBaseClasses/index';`; - const importEventsFromCatmod = (manifest: ICatmodManifest, catmodName: string): void => { if (manifest.events) { for (const eventName in manifest.events) { @@ -384,7 +381,6 @@ export { getEventByLib, splitEventName, getArgumentsTypeScript, - baseTypes, localizeCategoryName, localizeParametrized, canBeDynamicBehavior, diff --git a/src/riotTags/editors/script-editor.tag b/src/riotTags/editors/script-editor.tag index 9ee8c35b0..13331d259 100644 --- a/src/riotTags/editors/script-editor.tag +++ b/src/riotTags/editors/script-editor.tag @@ -50,9 +50,6 @@ script-editor.aPanel.aView.flexfix this.mixin(require('src/node_requires/riotMixins/wire').default); this.mixin(require('src/node_requires/riotMixins/discardio').default); - const eventsAPI = require('src/node_requires/events'); - const {baseTypes} = eventsAPI; - const {renamePropVar} = require('src/node_requires/catnip'); this.renamePropVar = e => { if (this.asset.language === 'catnip') { @@ -107,7 +104,7 @@ script-editor.aPanel.aView.flexfix monaco.editor.setModelLanguage(this.codeEditor.getModel(), this.asset.language); prevLanguage = this.asset.language; } - const codePrefix = `${baseTypes} function ctJsScript(options: Record) {`; + const codePrefix = 'function ctJsScript(options: Record) {'; this.codeEditor.setValue(this.asset.code); if (this.asset.language === 'typescript') { this.codeEditor.setWrapperCode(codePrefix, '}'); diff --git a/src/riotTags/shared/scriptables/code-editor-scriptable.tag b/src/riotTags/shared/scriptables/code-editor-scriptable.tag index a7772f309..e46827334 100644 --- a/src/riotTags/shared/scriptables/code-editor-scriptable.tag +++ b/src/riotTags/shared/scriptables/code-editor-scriptable.tag @@ -79,7 +79,6 @@ code-editor-scriptable.relative.wide.tall.flexcol window.orders.off('catnipGlobalVarRename', this.renamePropVar); }); - const {baseTypes} = eventsAPI; const updateEvent = () => { if (this.language === 'catnip') { return; @@ -100,14 +99,14 @@ code-editor-scriptable.relative.wide.tall.flexcol if (this.opts.asset.type === 'behavior') { ctEntity = this.opts.asset.behaviorType === 'template' ? 'BasicCopy' : - '(typeof Room)[\'prototype\']'; + 'Room'; } else if (this.opts.asset.type === 'room') { - ctEntity = '(typeof Room)[\'prototype\']'; + ctEntity = 'Room'; } else { // template, use the base class ctEntity = baseClassToTS[this.opts.asset.baseClass]; } const fields = eventsAPI.getFieldsTypeScript(this.opts.asset); - const codePrefix = `${baseTypes} function ctJsEvent(this: ${ctEntity}${fields}) {${varsDeclaration}`; + const codePrefix = `function ctJsEvent(this: ${ctEntity}${fields}) {${varsDeclaration}`; if (this.language === 'typescript') { this.codeEditor.setWrapperCode(codePrefix, '}'); } From 60d936da05df0f86024c7ba0f040a335ab84de58 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 27 Jun 2024 19:45:48 +0300 Subject: [PATCH 08/44] :pencil: Update readme.md --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f35ac6270..4b0faf6a7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,15 @@ **Ct.js is a 2D game engine and IDE** that aims to be powerful and flexible while still being easy to use and learn. It does that by providing extensive documentation, examples, a wide variety of game asset types and their visual editors — all while remaining open to modding, and if modding doesn't help, you can always put plain JS code in your project. -People **code in ct.js with TypeScript, JavaScript, or CoffeeScript**. Coding is built around the event system, and shared code is implemented as behaviors that can be combined and used by several object templates or levels (as opposed to inheritance). +| | | Ct.js features | | | +|-|-|-|-|-| +| Level & UI editor | Dynamic sound engine | Particle systems with a visual editor | Several scripting languages to choose from | Event-based scripting | +| One-click export for desktop platforms | One-click export for web | Ready for PC and mobile games | Fast, runs in WebGL | Free with no hidden subscribtions or fees | +| Frame-by-frame animations | Automatic atlas packing | Joystic support | Layout-agnostic input events | Tilemap support | +| Physics module | Arcade collision module | Local databases for complex data types | Behaviors for gameplay logic composition | Base classes for UI elements | +| Application branding | White-labelling | Add custom JS, CSS, or HTML | Flexible modular library | Extendable with special modules (catmods) | + +People **code in ct.js with TypeScript, JavaScript, CoffeeScript, or ct.js' visual scripting language Catnip**. Coding is built around the event system, and shared code is implemented as behaviors that can be combined and used by several object templates or levels (as opposed to inheritance). ## How ct.js and its games are made? @@ -55,8 +63,7 @@ For bugs, feature requests, development questions, please use [GitHub issues](ht * `src` — a source folder that compiles into `/app` folder at a build time. * `ct.release` — the ct.js game library, aka its "core" * `js` — different top-level scripts, including 3rd-party libraries. - * `node_requires` — built and copied to the `/app` directory. - * `pug` — HTML sources of editor's windows, written in [Pug](https://pugjs.org/). + * `node_requires` — shared JavaScript and TypeScript modules that cover exporter's functionality, asset management, utilities and such. * `riotTags` — components that drive UI logic in ct.js. Written in [Pug](https://pugjs.org/) and [Riot.js v3](https://v3.riotjs.now.sh/). * `styl` — style sheets, written in [Stylus](http://stylus-lang.com/). * `branding` — logos and icons belong here. @@ -67,10 +74,13 @@ For bugs, feature requests, development questions, please use [GitHub issues](ht ## Planning -See the [main dev board](https://github.com/orgs/ct-js/projects/5/views/7) for hot issues and plans for next releases. Prioritize the "Current release" column, then "To Do", then "Backlog", though if you really want a feature from a backlog to come true right here, right now, no one can stop you :) +Relatively large issues get posted in the [main dev board](https://github.com/orgs/ct-js/projects/5/views/7), along with issues that require help from the community. Prioritize the "Current release" column, then "To Do", then "Backlog", though if you really want a feature from a backlog to come true right here, right now, no one can stop you :) Please leave a comment on issues you want to work on so that we can assign you to them and avoid occasional double work from several contributors. +You can chat and discuss ct.js development in [ct.js' Discord server](https://comigo.games/discord), in #engine-development channel. + + ## Forking and installing the dev environment Building ct.js requires [Node and npm](https://nodejs.org/en/download/) installed on your machine. From 939a159e356b803f2e3e577dc01e346780c4ba99 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 27 Jun 2024 20:03:30 +0300 Subject: [PATCH 09/44] :pencil: :zap: Add icons to table of features --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b0faf6a7..031aab9e4 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ | | | Ct.js features | | | |-|-|-|-|-| -| Level & UI editor | Dynamic sound engine | Particle systems with a visual editor | Several scripting languages to choose from | Event-based scripting | -| One-click export for desktop platforms | One-click export for web | Ready for PC and mobile games | Fast, runs in WebGL | Free with no hidden subscribtions or fees | -| Frame-by-frame animations | Automatic atlas packing | Joystic support | Layout-agnostic input events | Tilemap support | -| Physics module | Arcade collision module | Local databases for complex data types | Behaviors for gameplay logic composition | Base classes for UI elements | -| Application branding | White-labelling | Add custom JS, CSS, or HTML | Flexible modular library | Extendable with special modules (catmods) | +| 🏗️ Level & UI editor | 🎶 Dynamic sound engine | ✨ Particle systems with a visual editor | 🧑‍💻 Several scripting languages to choose from | 🔔 Event-based scripting | +| 🖥️ One-click export for desktop platforms | 🌐 One-click export for web | 📱 Ready for PC and mobile games | 🔥 Fast, runs in WebGL | 💅 Free with no hidden subscribtions or fees | +| 🎞️ Frame-by-frame animations | 🗺️ Automatic atlas packing | 🕹️ Joystic support | ⌨️ Layout-agnostic input events | 🏁 Tilemap support | +| ⚽ Physics module | 👾 Arcade collision module | 🗂️ Local databases for complex data types | 🧠 Behaviors for gameplay logic composition | ✅ Base classes for UI elements | +| 🎨 Application branding | 🐻‍❄️ White-labelling | ➕ Add custom JS, CSS, or HTML | ⚙️ Flexible modular library | 📚 Extendable with special modules (catmods) | People **code in ct.js with TypeScript, JavaScript, CoffeeScript, or ct.js' visual scripting language Catnip**. Coding is built around the event system, and shared code is implemented as behaviors that can be combined and used by several object templates or levels (as opposed to inheritance). From 40643a3429fcb0ea1e84658965348efe83b058d4 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 28 Jun 2024 17:24:04 +0300 Subject: [PATCH 10/44] :bug: Fix backgrounds blocking click events on copies and tiles --- src/node_requires/roomEditor/entityClasses/Background.ts | 1 + src/node_requires/roomEditor/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/node_requires/roomEditor/entityClasses/Background.ts b/src/node_requires/roomEditor/entityClasses/Background.ts index d25ba3a70..5bffdf2d1 100644 --- a/src/node_requires/roomEditor/entityClasses/Background.ts +++ b/src/node_requires/roomEditor/entityClasses/Background.ts @@ -20,6 +20,7 @@ class Background extends PIXI.TilingSprite { constructor(bgInfo: IRoomBackground, editor: RoomEditor | RoomEditorPreview) { super(getPixiTexture(bgInfo.texture, 0, true)); + this.eventMode = 'none'; this.anchor.x = this.anchor.y = 0; this.editor = editor; this.deserialize(bgInfo); diff --git a/src/node_requires/roomEditor/index.ts b/src/node_requires/roomEditor/index.ts index a8504a768..38e1e1ee5 100644 --- a/src/node_requires/roomEditor/index.ts +++ b/src/node_requires/roomEditor/index.ts @@ -186,6 +186,7 @@ class RoomEditor extends PIXI.Application { this.mouseoverHint.zIndex = Infinity; this.mouseoverHint.visible = false; this.mouseoverHint.anchor.set(0, 1); + this.mouseoverHint.eventMode = 'none'; this.stage.addChild(this.mouseoverHint); this.ticker.add(() => { From 6017003d9c72bb1c1968cd7ccdd7029ce7c7e438 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 29 Jun 2024 14:26:09 +0300 Subject: [PATCH 11/44] :zap: Catnip: Add xprev, yprev blocks to the Movement category --- app/data/i18n/English.json | 13 +++++++++- src/node_requires/catnip/stdLib/movement.ts | 28 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index b56f4f1b2..d4e1a832d 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -285,6 +285,8 @@ "get direction": "direction", "y of copy": "y of a copy", "x of copy": "x of a copy", + "x prev": "previous x", + "y prev": "previous y", "get width": "width", "get height": "height", "set width": "Set width to", @@ -1804,12 +1806,21 @@ "Button": "Button", "RepeatingTexture": "Repeating texture", "SpritedCounter": "Sprited Counter", - "TextBox": "Text box" + "TextBox": "Text box", + "ScrollBox": "Scrollbox", + "Select": "Select menu", + "ItemList": "Item list" }, "nineSliceTop": "Top slice, in pixels", "nineSliceRight": "Right slice, in pixels", "nineSliceBottom": "Bottom slice, in pixels", "nineSliceLeft": "Left slice, in pixels", + "layoutItemsHeading": "Items layout", + "horizontalPadding": "Horizontal padding, in pixels", + "verticalPadding": "Vertical padding, in pixels", + "horizontalSpacing": "Horizontal item spacing, in pixels", + "verticalSpacing": "Vertical item spacing, in pixels", + "alignItems": "Align items:", "autoUpdateNineSlice": "Automatically update collision shape", "autoUpdateNineSliceHint": "If a panel changes its size, it will automatically update its collision shape. You usually don't need that for purely cosmetic elements, or the ones that never change their size after creation. You can still update its collision shape at any time with u.reshapeNinePatch(this) call.", "panelHeading": "Texture slicing settings", diff --git a/src/node_requires/catnip/stdLib/movement.ts b/src/node_requires/catnip/stdLib/movement.ts index 7cea5268a..b51dc67f7 100644 --- a/src/node_requires/catnip/stdLib/movement.ts +++ b/src/node_requires/catnip/stdLib/movement.ts @@ -271,6 +271,34 @@ const blocks: (IBlockCommandDeclaration | IBlockComputedDeclaration)[] = [{ lib: 'core.movement', code: 'x of' }] +}, { + name: 'x previous', + type: 'computed', + code: 'x prev', + icon: 'move', + i18nKey: 'x prev', + jsTemplate: () => 'this.xprev', + lib: 'core.movement', + pieces: [], + typeHint: 'number', + mutators: [{ + lib: 'core.movement', + code: 'y prev' + }] +}, { + name: 'y previous', + type: 'computed', + code: 'y prev', + icon: 'move', + i18nKey: 'y prev', + jsTemplate: () => 'this.yprev', + lib: 'core.movement', + pieces: [], + typeHint: 'number', + mutators: [{ + lib: 'core.movement', + code: 'x prev' + }] }]; export default blocks; From 33627a5aaa56e0271c36ca824ac1a60423b3686e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 29 Jun 2024 14:35:13 +0300 Subject: [PATCH 12/44] :bug: Fix mutators making broken blocks if a new `blocks` piece was introduced in a target block. Fixes errors with If-Else blocks that were mutated from If blocks --- src/node_requires/catnip/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/node_requires/catnip/index.ts b/src/node_requires/catnip/index.ts index bb5607368..114f9e70a 100644 --- a/src/node_requires/catnip/index.ts +++ b/src/node_requires/catnip/index.ts @@ -500,7 +500,8 @@ export const mutate = ( }, customOptions?: boolean ): void => { - const newBlock = blockFromDeclaration(getDeclaration(mutator.lib, mutator.code)); + const newDeclaration = getDeclaration(mutator.lib, mutator.code); + const newBlock = blockFromDeclaration(newDeclaration); if (Array.isArray(dest)) { const pos = key as number; migrateValues(dest[pos], newBlock); @@ -514,6 +515,11 @@ export const mutate = ( migrateValues(prevBlock, newBlock); dest.values[key] = newBlock; } + for (const piece of newDeclaration.pieces) { + if (piece.type === 'blocks' && !Array.isArray(newBlock.values[piece.key])) { + newBlock.values[piece.key] = []; + } + } }; export const emptyTexture = document.createElement('canvas'); emptyTexture.width = emptyTexture.height = 1; From 9abbfd88f18b8af5435e5f4ae27f8aabc2bbb16d Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 29 Jun 2024 15:38:52 +0300 Subject: [PATCH 13/44] :bug: Fix UI rooms positioned in reverse coordinate system when using this.x, this.y instead of this.position.x, this.position.y --- src/ct.release/rooms.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ct.release/rooms.ts b/src/ct.release/rooms.ts index 567cdbfb6..1d1f619ec 100644 --- a/src/ct.release/rooms.ts +++ b/src/ct.release/rooms.ts @@ -316,15 +316,27 @@ export class Room extends PIXI.Container { return this; } get x(): number { + if (this.isUi) { + return this.position.x; + } return -this.position.x; } set x(value: number) { + if (this.isUi) { + this.position.x = value; + } this.position.x = -value; } get y(): number { + if (this.isUi) { + return this.position.y; + } return -this.position.y; } set y(value: number) { + if (this.isUi) { + this.position.y = value; + } this.position.y = -value; } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 04acd477f3333c1e518e39ed71b840a6f59b76a1 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 29 Jun 2024 15:41:02 +0300 Subject: [PATCH 14/44] :bug: Fix nested copies not being removed from appended/prepended rooms when a user calls `rooms.remove` on them. --- src/ct.release/index.ts | 35 +---------------------------------- src/ct.release/rooms.ts | 4 ++-- src/ct.release/templates.ts | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/ct.release/index.ts b/src/ct.release/index.ts index c3f4da1cb..d61e9fb78 100644 --- a/src/ct.release/index.ts +++ b/src/ct.release/index.ts @@ -16,7 +16,7 @@ import resM from 'res'; import roomsM, {Room} from './rooms'; import soundsM from 'sounds'; import stylesM from 'styles'; -import templatesM, {BasicCopy} from './templates'; +import templatesM, {BasicCopy, killRecursive} from './templates'; import tilemapsM, {Tilemap} from './tilemaps'; import timerM from './timer'; import {scriptsLib as scriptsM} from './scripts'; @@ -186,39 +186,6 @@ export let pixiApp: pixiMod.Application; let loading: Promise; { - const killRecursive = (copy: (BasicCopy & pixiMod.DisplayObject) | Background) => { - copy.kill = true; - if (templatesM.isCopy(copy) && (copy as BasicCopy).onDestroy) { - templatesM.onDestroy.apply(copy); - (copy as BasicCopy).onDestroy.apply(copy); - } - if (copy.children) { - for (const child of copy.children) { - if (templatesM.isCopy(child)) { - killRecursive(child as (BasicCopy & pixiMod.DisplayObject)); // bruh - } - } - } - const stackIndex = stack.indexOf(copy); - if (stackIndex !== -1) { - stack.splice(stackIndex, 1); - } - if (templatesM.isCopy(copy) && (copy as BasicCopy).template) { - if ((copy as BasicCopy).template) { - const {template} = (copy as BasicCopy); - if (template) { - const templatelistIndex = templatesM - .list[template] - .indexOf((copy as BasicCopy)); - if (templatelistIndex !== -1) { - templatesM.list[template] - .splice(templatelistIndex, 1); - } - } - } - } - deadPool.push(copy); - }; const manageCamera = () => { cameraM.update(uM.timeUi); cameraM.manageStage(); diff --git a/src/ct.release/rooms.ts b/src/ct.release/rooms.ts index 1d1f619ec..a708ccbf9 100644 --- a/src/ct.release/rooms.ts +++ b/src/ct.release/rooms.ts @@ -1,6 +1,6 @@ import uLib from './u'; import backgrounds, {Background} from './backgrounds'; -import templatesLib, {BasicCopy} from './templates'; +import templatesLib, {BasicCopy, killRecursive} from './templates'; import {Tilemap} from './tilemaps'; import mainCamera from './camera'; import {copyTypeSymbol, deadPool, pixiApp, stack, forceDestroy} from '.'; @@ -429,7 +429,7 @@ const roomsLib = { pixiApp.stage.removeChild(room); for (const copy of room.children) { if (copyTypeSymbol in copy) { - (copy as BasicCopy).kill = true; + killRecursive(copy as BasicCopy); } } room.onLeave(); diff --git a/src/ct.release/templates.ts b/src/ct.release/templates.ts index 569498daf..40530d3d0 100644 --- a/src/ct.release/templates.ts +++ b/src/ct.release/templates.ts @@ -4,7 +4,7 @@ import {Background} from './backgrounds'; import {Tilemap} from './tilemaps'; import roomsLib, {Room} from './rooms'; import {runBehaviors} from './behaviors'; -import {copyTypeSymbol, stack} from '.'; +import {copyTypeSymbol, stack, deadPool} from '.'; import uLib from './u'; import type * as pixiMod from 'pixi.js'; @@ -424,6 +424,40 @@ export const makeCopy = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any }; +export const killRecursive = (copy: (BasicCopy & pixiMod.DisplayObject) | Background) => { + copy.kill = true; + if (templatesLib.isCopy(copy) && (copy as BasicCopy).onDestroy) { + templatesLib.onDestroy.apply(copy); + (copy as BasicCopy).onDestroy.apply(copy); + } + if (copy.children) { + for (const child of copy.children) { + if (templatesLib.isCopy(child)) { + killRecursive(child as (BasicCopy & pixiMod.DisplayObject)); // bruh + } + } + } + const stackIndex = stack.indexOf(copy); + if (stackIndex !== -1) { + stack.splice(stackIndex, 1); + } + if (templatesLib.isCopy(copy) && (copy as BasicCopy).template) { + if ((copy as BasicCopy).template) { + const {template} = (copy as BasicCopy); + if (template) { + const templatelistIndex = templatesLib + .list[template] + .indexOf((copy as BasicCopy)); + if (templatelistIndex !== -1) { + templatesLib.list[template] + .splice(templatelistIndex, 1); + } + } + } + } + deadPool.push(copy); +}; + const onCreateModifier = function () { /*!%oncreate%*/ }; From 99afa015d0ced9097a30568f12240980a0b59c38 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych aka CoMiGo Date: Sun, 30 Jun 2024 16:16:18 +0300 Subject: [PATCH 15/44] Update stale.yml --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index d44497a17..ed18da08e 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,6 +6,7 @@ daysUntilClose: 90 exemptLabels: - 'type:epic' - 'type:debt' + - eternal # Label to use when marking an issue as stale staleLabel: 'state:stale' # Comment to post when marking an issue as stale. Set to `false` to disable From e4ca36287d6f1e7b82a6d248af181a027499d630 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 30 Jun 2024 17:21:10 +0300 Subject: [PATCH 16/44] :bug: Fix ct.transition causing an infinite recursion of room removal in its transitions --- app/data/ct.libs/transition/injections/templates.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/data/ct.libs/transition/injections/templates.js b/app/data/ct.libs/transition/injections/templates.js index 219d5434a..91a070175 100644 --- a/app/data/ct.libs/transition/injections/templates.js +++ b/app/data/ct.libs/transition/injections/templates.js @@ -25,7 +25,7 @@ void 0; }, onDestroy() { - rooms.remove(this.room); + void 0; }, onCreate() { this.tex = -1; @@ -58,7 +58,7 @@ void 0; }, onDestroy() { - rooms.remove(this.room); + void 0; }, onCreate() { this.tex = -1; @@ -110,7 +110,7 @@ void 0; }, onDestroy() { - rooms.remove(this.room); + void 0; }, onCreate() { this.tex = -1; @@ -211,7 +211,7 @@ void 0; }, onDestroy() { - rooms.remove(this.room); + void 0; }, onCreate() { this.tex = -1; From a57b51377405dd0db43eaeef0b91c2882683893e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sun, 30 Jun 2024 17:58:47 +0300 Subject: [PATCH 17/44] :zap: Use fixed port number for in-editor docs and debugger so that localStorage doesn't vanish on ct.js restart. Also fixes memory leak with lingering web servers after closing a project. --- src/riotTags/app-view.tag | 18 +++++++++++++++++- src/riotTags/notepad-panel.tag | 19 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag index 0f8fa1e99..692950e7b 100644 --- a/src/riotTags/app-view.tag +++ b/src/riotTags/app-view.tag @@ -380,6 +380,7 @@ app-view.flexcol const {getExportDir} = require('src/node_requires/platformUtils'); // Run a local server for ct.js games let fileServer; + const debuggerPort = 40469; if (!this.debugServerStarted) { getExportDir().then(dir => { const fileServerSettings = { @@ -389,13 +390,28 @@ app-view.flexcol const handler = require('serve-handler'); fileServer = require('http').createServer((request, response) => handler(request, response, fileServerSettings)); - fileServer.listen(0, () => { + fileServer.on('error', e => { + if (e.code === 'EADDRINUSE') { + fileServer.close(); + fileServer.listen(0); + } else { + throw e; + } + }); + fileServer.listen(debuggerPort, () => { // eslint-disable-next-line no-console console.info(`[ct.debugger] Running dev server at http://localhost:${fileServer.address().port}`); }); + this.debugServer = fileServer; this.debugServerStarted = true; }); } + this.on('unmount', () => { + if (this.debugServer) { + this.debugServer.close(); + this.debugServer.closeAllConnections(); + } + }); // Options when there are unapplied assets but a user triggers a launch this.showPrelaunchSave = false; diff --git a/src/riotTags/notepad-panel.tag b/src/riotTags/notepad-panel.tag index d47b5e9ee..87f5742c8 100644 --- a/src/riotTags/notepad-panel.tag +++ b/src/riotTags/notepad-panel.tag @@ -22,7 +22,7 @@ notepad-panel#notepad.aPanel.dockright(class="{opened: opened}") div(show="{tab === 'notepadglobal'}") .aCodeEditor(ref="notepadglobal") div(show="{tab === 'helppages'}") - iframe(src="http://localhost:{server.address().port}/{getIfDarkTheme()? '?darkTheme=yep' : ''}" ref="helpIframe" nwdisable nwfaketop) + iframe(if="{server && server.address()}" src="http://localhost:{server.address().port}/{getIfDarkTheme()? '?darkTheme=yep' : ''}" ref="helpIframe" nwdisable nwfaketop) button.aHomeButton(title="{voc.backToHome}" onclick="{backToHome}") svg.feather use(xlink:href="#home") @@ -127,20 +127,35 @@ notepad-panel#notepad.aPanel.dockright(class="{opened: opened}") // Manually destroy the editors to free up the memory this.notepadlocal.dispose(); this.notepadglobal.dispose(); + if (this.server) { + this.server.close(); + this.server.closeAllConnections(); + } }); const fileServerSettings = { public: 'data/docs/', cleanUrls: true }; + + const docsPort = 40470; const handler = require('serve-handler'); if (!this.docServerStarted) { const fileServer = require('http').createServer((request, response) => handler(request, response, fileServerSettings)); - fileServer.listen(0, () => { + const startupListener = () => { // eslint-disable-next-line no-console console.info(`[ct.docs] Running docs server at http://localhost:${fileServer.address().port}`); + }; + fileServer.on('error', e => { + if (e.code === 'EADDRINUSE') { + fileServer.close(); + fileServer.listen(0); + } else { + throw e; + } }); + fileServer.listen(docsPort, startupListener); this.server = fileServer; this.docServerStarted = true; } From 83f22db38dc78ff260a60c1b331714309d2babfd Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 5 Jul 2024 11:20:29 +1200 Subject: [PATCH 18/44] :bug: Fetch patrons list on devSetup so there're no cache errors while developing locally --- devSetup.gulpfile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devSetup.gulpfile.js b/devSetup.gulpfile.js index 5beba9d03..9a9d7be23 100644 --- a/devSetup.gulpfile.js +++ b/devSetup.gulpfile.js @@ -58,6 +58,11 @@ const makeVSCodeLaunchJson = () => { const fetchNeutralino = async () => (await import('./gulpfile.mjs')).fetchNeutralino(); +const fetchPatrons = async () => { + const {patronsCache} = await import('./gulpfile.mjs'); + return patronsCache; +}; + const defaultTask = gulp.series([ updateGitSubmodules, gulp.parallel([ @@ -69,6 +74,7 @@ const defaultTask = gulp.series([ gulp.parallel([ bakeDocs, makeVSCodeLaunchJson, + fetchPatrons, fetchNeutralino ]) ]); From a7ffa5dc2563ffc0e279e910e54abf4f1d4b664c Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 10 Jul 2024 20:34:22 +1200 Subject: [PATCH 19/44] :bug: :bento: Fix sound recorder by replacing microm package with @tscole/mic-recorder-to-mp3 --- package-lock.json | 124 +++++++++++------- package.json | 2 +- .../editors/sound-editor/sound-recorder.tag | 22 ++-- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52e36870d..aade0b09e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@pixi/particle-emitter": "5.0.8", "@pixi/sound": "^5.2.2", "@trapezedev/project": "^7.0.10", + "@tscole/mic-recorder-to-mp3": "^2.2.4", "adm-zip": "^0.5.12", "coffeescript": "^2.7.0", "csswring": "7.0.0", @@ -30,7 +31,6 @@ "js-yaml": "^3.14.0", "markdown-it": "12.3.2", "maxrects-packer": "^2.7.3", - "microm": "^0.2.4", "monaco-editor": "^0.47.0", "monaco-themes": "^0.4.4", "nanoid": "^3.1.31", @@ -3082,6 +3082,17 @@ "node": ">=12" } }, + "node_modules/@tscole/mic-recorder-to-mp3": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tscole/mic-recorder-to-mp3/-/mic-recorder-to-mp3-2.2.4.tgz", + "integrity": "sha512-mow8CMLhYoblOYMFCFCcJaJlUSs9m2mazF0yYN77+Zylv9LaPDNEHQyT7rjLvowk6o5hrSM4pKThyVv6dO6cqg==", + "dependencies": { + "lamejs": "^1.2.0" + }, + "peerDependencies": { + "webrtc-adapter": ">=4.1.1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -13351,6 +13362,14 @@ "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==", "dev": true }, + "node_modules/lamejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.1.tgz", + "integrity": "sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==", + "dependencies": { + "use-strict": "1.0.1" + } + }, "node_modules/last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", @@ -14560,20 +14579,6 @@ "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", "integrity": "sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==" }, - "node_modules/microm": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/microm/-/microm-0.2.4.tgz", - "integrity": "sha512-crhCgZZ+bu1nswOTfjAMQDt2KO++nsv9WLPP/Bn8FMqZoGIxASqCpmAD757K1VFGyTVnbe6cwOmaRFKNr9vYqg==", - "dependencies": { - "extend": "3.0.0", - "rsvp": "^3.1.0" - } - }, - "node_modules/microm/node_modules/extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha512-5mYyg57hpD+sFaJmgNL9BidQ5C7dmJE3U5vzlRWbuqG+8dytvYEoxvKs6Tj5cm3LpMsFvRt20qz1ckezmsOUgQ==" - }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -20162,14 +20167,6 @@ "rollup": "bin/rollup" } }, - "node_modules/rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", - "engines": { - "node": "0.12.* || 4.* || 6.* || >= 7.*" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20216,6 +20213,12 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "node_modules/sdp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", + "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==", + "peer": true + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -22985,6 +22988,11 @@ "node": ">=0.10.0" } }, + "node_modules/use-strict": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz", + "integrity": "sha512-IeiWvvEXfW5ltKVMkxq6FvNf2LojMKvB2OCeja6+ct24S1XOmQw2dGr2JyndwACWAGJva9B7yPHwAmeA9QCqAQ==" + }, "node_modules/use/node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -23225,6 +23233,19 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webrtc-adapter": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz", + "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==", + "peer": true, + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -25540,6 +25561,14 @@ } } }, + "@tscole/mic-recorder-to-mp3": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tscole/mic-recorder-to-mp3/-/mic-recorder-to-mp3-2.2.4.tgz", + "integrity": "sha512-mow8CMLhYoblOYMFCFCcJaJlUSs9m2mazF0yYN77+Zylv9LaPDNEHQyT7rjLvowk6o5hrSM4pKThyVv6dO6cqg==", + "requires": { + "lamejs": "^1.2.0" + } + }, "@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -33464,6 +33493,14 @@ "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==", "dev": true }, + "lamejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.1.tgz", + "integrity": "sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==", + "requires": { + "use-strict": "1.0.1" + } + }, "last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", @@ -34454,22 +34491,6 @@ "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", "integrity": "sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==" }, - "microm": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/microm/-/microm-0.2.4.tgz", - "integrity": "sha512-crhCgZZ+bu1nswOTfjAMQDt2KO++nsv9WLPP/Bn8FMqZoGIxASqCpmAD757K1VFGyTVnbe6cwOmaRFKNr9vYqg==", - "requires": { - "extend": "3.0.0", - "rsvp": "^3.1.0" - }, - "dependencies": { - "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha512-5mYyg57hpD+sFaJmgNL9BidQ5C7dmJE3U5vzlRWbuqG+8dytvYEoxvKs6Tj5cm3LpMsFvRt20qz1ckezmsOUgQ==" - } - } - }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -38680,11 +38701,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.51.8.tgz", "integrity": "sha512-e7FwWxqb4vhdonmwRH06nqC9wR6h1kZojK2D+lN1xjiB8FDtAKgy7o+r8fCXVzQZ1ZCdcVlls3mTq5g6u38Jew==" }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -38717,6 +38733,12 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "sdp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", + "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==", + "peer": true + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -40843,6 +40865,11 @@ } } }, + "use-strict": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz", + "integrity": "sha512-IeiWvvEXfW5ltKVMkxq6FvNf2LojMKvB2OCeja6+ct24S1XOmQw2dGr2JyndwACWAGJva9B7yPHwAmeA9QCqAQ==" + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -41053,6 +41080,15 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "webrtc-adapter": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz", + "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==", + "peer": true, + "requires": { + "sdp": "^3.2.0" + } + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index acb319c19..da55b5b75 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@pixi/particle-emitter": "5.0.8", "@pixi/sound": "^5.2.2", "@trapezedev/project": "^7.0.10", + "@tscole/mic-recorder-to-mp3": "^2.2.4", "adm-zip": "^0.5.12", "coffeescript": "^2.7.0", "csswring": "7.0.0", @@ -42,7 +43,6 @@ "js-yaml": "^3.14.0", "markdown-it": "12.3.2", "maxrects-packer": "^2.7.3", - "microm": "^0.2.4", "monaco-editor": "^0.47.0", "monaco-themes": "^0.4.4", "nanoid": "^3.1.31", diff --git a/src/riotTags/editors/sound-editor/sound-recorder.tag b/src/riotTags/editors/sound-editor/sound-recorder.tag index 5f7bdb3f9..11cfe5ed0 100644 --- a/src/riotTags/editors/sound-editor/sound-recorder.tag +++ b/src/riotTags/editors/sound-editor/sound-recorder.tag @@ -1,6 +1,6 @@ // - @attribute sound - @atribute onclose + @attribute sound (ISound) + @atribute onclose (() => void) sound-recorder.aDimmer.fadein .aModal.pad.appear @@ -111,7 +111,7 @@ sound-recorder.aDimmer.fadein }); var mp3Recorder; - const Microm = require('microm'); + const Microm = require('@tscole/mic-recorder-to-mp3'); this.on('unmount', () => { if (mp3Recorder) { mp3Recorder.stop(); @@ -119,9 +119,12 @@ sound-recorder.aDimmer.fadein } }); this.startRecording = async () => { - mp3Recorder = new Microm(); + mp3Recorder = new Microm({ + bitRate: 128, + audio: true + }); try { - await mp3Recorder.record(); + await mp3Recorder.start(); this.state = 'recording'; } catch (ohNo) { console.error(ohNo); @@ -133,7 +136,9 @@ sound-recorder.aDimmer.fadein this.state = 'loading'; this.update(); await mp3Recorder.stop(); - this.previewAudioUrl = mp3Recorder.getUrl(); + const [, blob] = await mp3Recorder.getMp3(); + this.previewBlob = blob; + this.previewAudioUrl = URL.createObjectURL(blob); this.state = 'editing'; this.update(); }; @@ -146,10 +151,9 @@ sound-recorder.aDimmer.fadein const sounds = require('src/node_requires/resources/sounds'); const path = require('path'), fs = require('fs-extra'); - const base64 = (await mp3Recorder.getBase64()).replace('data:audio/mp3;base64,', ''); - const buffer = Buffer.from(base64, 'base64'); const temp = await require('src/node_requires/platformUtils').getTempDir(); - await fs.writeFile(path.join(temp.dir, 'recording.mp3'), buffer); + const ab = await this.previewBlob.arrayBuffer(); + await fs.writeFile(path.join(temp.dir, 'recording.mp3'), Buffer.from(ab)); await sounds.addSoundFile(this.opts.sound, path.join(temp.dir, 'recording.mp3')); temp.remove(); this.state = 'ready'; From 4f5cdb737cc35d60d868c23eb7079939f476b5f6 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 10 Jul 2024 21:32:44 +1200 Subject: [PATCH 20/44] :bug: Add missing translation keys for actions --- app/data/i18n/English.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index d4e1a832d..508cb1aef 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -354,6 +354,10 @@ "this": "this", "concatenate strings": "Concatenate strings", "concatenate strings triple": "Concatenate strings (triple)", + "action value": "action value", + "is action pressed": "action is pressed", + "is action down": "action is down", + "is action released": "action was released", "templates Templates copy into room": "Copy a template into room", "templates Templates copy": "Copy a template", "templates Templates each": "For each copy", From be47c7b76774ac85f21da3f5b0ef2fc3847bf5db Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 10 Jul 2024 21:50:32 +1200 Subject: [PATCH 21/44] :globe_with_meridians: Update Russian translation files --- app/data/i18n/Russian.json | 65 ++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index fcbd21e0c..9cf5a83d1 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -132,6 +132,11 @@ "скрипт", "скрипта", "скриптов" + ], + "enum": [ + "перечисление", + "перечисления", + "перечислений" ] }, "next": "Далее", @@ -192,7 +197,7 @@ "math": "Математика", "sounds": "Звуки", "movement": "Движение", - "appearance": "Appearance", + "appearance": "Внешний вид", "rooms": "Комнаты", "behaviors": "Поведения", "backgrounds": "Фоны", @@ -283,6 +288,10 @@ "set animation speed": "Задать скорость анимации", "this": "я", "concatenate strings": "Соединить строки", + "action value": "значение действия", + "is action pressed": "действие было нажато", + "is action down": "действие активно", + "is action released": "действие отпущено", "templates Templates copy into room": "Скопировать шаблон в комнату", "templates Templates copy": "Скопировать шаблон", "templates Templates each": "Для каждой копии", @@ -491,7 +500,10 @@ "delete from storage": "Удалить ключ", "load from storage": "загрузить из ключа", "is key in storage": "ключ в хранилище?", - "owning room": "комната-родитель копии" + "owning room": "комната-родитель копии", + "x prev": "предыдущий x", + "y prev": "предыдущий y", + "content type entries": "записи контента" }, "blockDisplayNames": { "if else branch": "Если", @@ -583,7 +595,15 @@ "blockDocumentation": { "serialize object": "Этот блок сериализирует объект в строку, которую позднее можно безопасно сохранить или передать. Этот метод не поддерживает объект Date, функции, и структуры с цикличными ссылками. Объекты, сериализированные этим блоком, должны десериализироваться блоком \"Десериализировать объекта\"", "constant string": "Этот блок можно использовать, чтобы явно создать строку: например, когда нужно написать числа в строке или конвертировать \\n в перенос строки при записи переменной в слот-джокер." - } + }, + "globalVariables": "Глобальные переменные", + "globalVariablesHint": "Эти переменные доступны из любой точки твоего проекта. Они не сохраняются после завершения игры — для этого используй блоки \"Сохранить под ключом\" и \"Загрузить из ключа\".", + "createNewGlobalVariable": "Новая глобальная переменная", + "newGlobalVariablePrompt": "Введи название новой глобальной переменной. В имени переменной не должно быть пробелов или спецсимволов, должно быть написано латиницей и может содержать цифры после первого символа.", + "invalidVarNameError": "Неверное имя переменной. В имени переменной не должно быть пробелов или спецсимволов, и оно должно начинаться с буквы.", + "renamePropertyPrompt": "Введи новое имя этого свойства:", + "renameVariablePrompt": "Введи новое имя этой переменной:", + "renamingAcrossProject": "Заменяю имя переменной в других ассетах…" }, "colorPicker": { "old": "Старый", @@ -812,7 +832,8 @@ "deleteScript": "Удалить этот скрипт", "moveDown": "Поставить ниже", "moveUp": "Поставить выше", - "newScriptComment": "Используйте скрипты для создания функций и импорта небольших библиотек" + "newScriptComment": "Используйте скрипты для создания функций и импорта небольших библиотек", + "scriptsHint": "Созданные здесь скрипты будут вставлены в корень игры и потому всегда сработают при старте игры. Поддерживаются только JavaScript и TypeScript. Указанные здесь типы и переменные будут доступны везде в твоём проекте." }, "modules": { "heading": "Котомоды" @@ -833,7 +854,11 @@ "assetTreeNote": "Ты можешь экспортировать организацию ассетов в игру как поле res.tree, но это делает структуру исходного проекта видимой и добавляет чуть-чуть веса экспортируемым проектам.", "exportAssetTree": "Экспортировать дерево ассетов", "exportAssetTypes": "Экспортировать только эти ассеты:", - "autocloseDesktop": "Выйти из приложения при нажатии кнопки \"Закрыть\"." + "autocloseDesktop": "Выйти из приложения при нажатии кнопки \"Закрыть\".", + "errorReporting": "Отчёты об ошибках", + "showErrors": "Показывать ошибки в окне игры (рекомендуется)", + "showErrorsHint": "Ct.js отобразит неотловленные исключения в своём окне, из которого игроки смогут скопировать текст ошибки и перейти по ссылке, которую ты можешь указать в следующем поле. Это поможет игрокам докладывать об ошибках без использования инструментов разработчика.", + "errorsLink": "Ссылка для отправки ошибок: (Github, контактная форма, форум и т.п.)" }, "content": { "heading": "Редактор типов контента", @@ -856,9 +881,15 @@ "gotoEntries": "Перейти к записям", "entries": "Записи", "structureTypes": { - "array": "Массив" + "array": "Массив", + "atomic": "Одно значение", + "map": "Словарь" }, - "fixedLength": "Фиксированный размер" + "fixedLength": "Фиксированный размер", + "fieldStructure": "Структура", + "key": "Ключ", + "value": "Значение", + "mappedType": "Тип для значений" }, "contentTypes": "Типы контента", "main": { @@ -1260,7 +1291,8 @@ "unsavedAssets": "Несохранённые ассеты:", "runWithoutApplying": "Запустить как есть", "applyAndRun": "Применить и запустить", - "cantAddEditor": "Нельзя добавить новый редактор. Закрой несколько вкладок с комнатами, стилями, или редакторами тандемов." + "cantAddEditor": "Нельзя добавить новый редактор. Закрой несколько вкладок с комнатами, стилями, или редакторами тандемов.", + "loadingPreviouslyOpened": "Открываю ранее открытые ассеты…" }, "assetViewer": { "root": "Корень", @@ -1582,7 +1614,10 @@ "RepeatingTexture": "Повторяющаяся текстура", "SpritedCounter": "Спрайтовый счётчик", "TextBox": "Поле ввода", - "BitmapText": "Битмап-текст" + "BitmapText": "Битмап-текст", + "ScrollBox": "Контейнер с прокруткой", + "Select": "Меню выбора", + "ItemList": "Список" }, "nineSliceTop": "Верхний срез, в пикселях", "nineSliceRight": "Правый срез, в пикселях", @@ -1605,7 +1640,13 @@ "isUi": "Использовать UI время", "defaultCount": "Кол-во спрайтов по умолчанию:", "useBitmapText": "Использовать битмап-шрифты", - "errorBitmapNotConfigured": "Выбранный стиль не привязан к семейству шрифтов с битмап-шрифтом. Открой этот стиль, привяжи его к семейству шрифтов, и убедись, что выбранное семейство шрифтов настроено для экспорта битмап-шрифтов. После этого примени изменения." + "errorBitmapNotConfigured": "Выбранный стиль не привязан к семейству шрифтов с битмап-шрифтом. Открой этот стиль, привяжи его к семейству шрифтов, и убедись, что выбранное семейство шрифтов настроено для экспорта битмап-шрифтов. После этого примени изменения.", + "layoutItemsHeading": "Вёрстка элементов", + "horizontalPadding": "Горизонтальная отбивка, в пикс.", + "verticalPadding": "Вертикальная отбивка, в пикс.", + "horizontalSpacing": "Отступ элементов по горизонтали, в пикс.", + "verticalSpacing": "Отступ элементов по вертикали, в пикс.", + "alignItems": "Выравнивать элементы:" }, "assetInput": { "changeAsset": "Нажмите, чтобы заменить ассет", @@ -1787,5 +1828,9 @@ "convertToJavaScript": "Конвертировать в JavaScript", "confirmSwitchToCatnip": "Переключение на Котомяту удалит весь код в этом скрипте. Ты точно хочешь продолжить?", "confirmSwitchFromCatnip": "Переключение с котомяты удалит весь код в этом скрипте. Если ты хочешь сохранить код, сконвертируй его в JavaScript. Ты точно хочешь продолжить и очистить код?" + }, + "enumEditor": { + "addVariant": "Добавить вариант", + "enumUseCases": "Это перечисление будет доступно во всём коде твоего проекта и будет выступать как тип данных в типах контента и пользовательских полях поведений." } } From 9c4436f6d2fb4f8f52abef7010b3dea4f7151904 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Wed, 10 Jul 2024 21:50:48 +1200 Subject: [PATCH 22/44] :globe_with_meridians: Update debug and comments translation files --- app/data/i18n/Comments.json | 68 +++++++++++++++++++++++++++++++------ app/data/i18n/Debug.json | 68 +++++++++++++++++++++++++++++++------ 2 files changed, 114 insertions(+), 22 deletions(-) diff --git a/app/data/i18n/Comments.json b/app/data/i18n/Comments.json index a85a821f8..9f6cb0575 100644 --- a/app/data/i18n/Comments.json +++ b/app/data/i18n/Comments.json @@ -128,6 +128,11 @@ "", "", "" + ], + "enum": [ + "", + "", + "" ] }, "next": "", @@ -590,7 +595,8 @@ "deleteScript": "", "moveDown": "", "moveUp": "", - "newScriptComment": "" + "newScriptComment": "", + "scriptsHint": "" }, "catmodsSettings": "", "export": { @@ -608,7 +614,11 @@ "assetTreeNote": "", "exportAssetTree": "", "exportAssetTypes": "", - "autocloseDesktop": "" + "autocloseDesktop": "", + "errorReporting": "", + "showErrors": "", + "showErrorsHint": "", + "errorsLink": "" }, "content": { "heading": "", @@ -630,8 +640,16 @@ "confirmDeletionMessage": "", "gotoEntries": "", "entries": "", - "array": "", - "fixedLength": "" + "fixedLength": "", + "fieldStructure": "", + "structureTypes": { + "atomic": "", + "array": "", + "map": "" + }, + "key": "", + "value": "", + "mappedType": "" }, "contentTypes": "", "main": { @@ -820,7 +838,8 @@ "unsavedAssets": "", "runWithoutApplying": "", "applyAndRun": "", - "cantAddEditor": "" + "cantAddEditor": "", + "loadingPreviouslyOpened": "" }, "assetViewer": { "root": "", @@ -1151,7 +1170,10 @@ "RepeatingTexture": "", "SpritedCounter": "", "TextBox": "", - "BitmapText": "" + "BitmapText": "", + "ScrollBox": "", + "Select": "", + "ItemList": "" }, "nineSliceTop": "", "nineSliceRight": "", @@ -1165,7 +1187,13 @@ "isUi": "To use u.time or u.timeUi", "defaultCount": "A label for a field that sets how many sprites are shown when no value has been set through code.", "useBitmapText": "", - "errorBitmapNotConfigured": "" + "errorBitmapNotConfigured": "", + "layoutItemsHeading": "", + "horizontalPadding": "", + "verticalPadding": "", + "horizontalSpacing": "", + "verticalSpacing": "", + "alignItems": "" }, "assetInput": { "changeAsset": "", @@ -1403,7 +1431,6 @@ "settings": "", "sounds": "", "strings": "", - "styles": "", "templates": "", "utilities": "", "actions": "", @@ -1693,7 +1720,14 @@ "delete from storage": "", "load from storage": "", "is key in storage": "", - "owning room": "" + "owning room": "", + "x prev": "", + "y prev": "", + "content type entries": "", + "action value": "", + "is action pressed": "", + "is action down": "", + "is action released": "" }, "blockDisplayNames": { "write": "", @@ -1781,10 +1815,22 @@ "blockDocumentation": { "serialize object": "", "constant string": "" - } + }, + "globalVariables": "", + "globalVariablesHint": "", + "createNewGlobalVariable": "", + "newGlobalVariablePrompt": "", + "invalidVarNameError": "", + "renamePropertyPrompt": "", + "renameVariablePrompt": "", + "renamingAcrossProject": "" }, "regionalLinks": { "discord": "", "telegram": "" + }, + "enumEditor": { + "addVariant": "", + "enumUseCases": "" } -} \ No newline at end of file +} diff --git a/app/data/i18n/Debug.json b/app/data/i18n/Debug.json index b925ac60a..d4d739334 100644 --- a/app/data/i18n/Debug.json +++ b/app/data/i18n/Debug.json @@ -128,6 +128,11 @@ "common.assetTypes.script.0", "common.assetTypes.script.1", "common.assetTypes.script.2" + ], + "enum": [ + "common.assetTypes.enum.0", + "common.assetTypes.enum.1", + "common.assetTypes.enum.2" ] }, "next": "common.next", @@ -590,7 +595,8 @@ "deleteScript": "settings.scripts.deleteScript", "moveDown": "settings.scripts.moveDown", "moveUp": "settings.scripts.moveUp", - "newScriptComment": "settings.scripts.newScriptComment" + "newScriptComment": "settings.scripts.newScriptComment", + "scriptsHint": "settings.scripts.scriptsHint" }, "catmodsSettings": "settings.catmodsSettings", "export": { @@ -608,7 +614,11 @@ "assetTreeNote": "settings.export.assetTreeNote", "exportAssetTree": "settings.export.exportAssetTree", "exportAssetTypes": "settings.export.exportAssetTypes", - "autocloseDesktop": "settings.export.autocloseDesktop" + "autocloseDesktop": "settings.export.autocloseDesktop", + "errorReporting": "settings.export.errorReporting", + "showErrors": "settings.export.showErrors", + "showErrorsHint": "settings.export.showErrorsHint", + "errorsLink": "settings.export.errorsLink" }, "content": { "heading": "settings.content.heading", @@ -630,8 +640,16 @@ "confirmDeletionMessage": "settings.content.confirmDeletionMessage", "gotoEntries": "settings.content.gotoEntries", "entries": "settings.content.entries", - "array": "settings.content.array", - "fixedLength": "settings.content.fixedLength" + "fixedLength": "settings.content.fixedLength", + "fieldStructure": "settings.content.fieldStructure", + "structureTypes": { + "atomic": "settings.content.structureTypes.atomic", + "array": "settings.content.structureTypes.array", + "map": "settings.content.structureTypes.map" + }, + "key": "settings.content.key", + "value": "settings.content.value", + "mappedType": "settings.content.mappedType" }, "contentTypes": "settings.contentTypes", "main": { @@ -820,7 +838,8 @@ "unsavedAssets": "appView.unsavedAssets", "runWithoutApplying": "appView.runWithoutApplying", "applyAndRun": "appView.applyAndRun", - "cantAddEditor": "appView.cantAddEditor" + "cantAddEditor": "appView.cantAddEditor", + "loadingPreviouslyOpened": "appView.loadingPreviouslyOpened" }, "assetViewer": { "root": "assetViewer.root", @@ -1151,7 +1170,10 @@ "RepeatingTexture": "templateView.baseClass.RepeatingTexture", "SpritedCounter": "templateView.baseClass.SpritedCounter", "TextBox": "templateView.baseClass.TextBox", - "BitmapText": "templateView.baseClass.BitmapText" + "BitmapText": "templateView.baseClass.BitmapText", + "ScrollBox": "templateView.baseClass.ScrollBox", + "Select": "templateView.baseClass.Select", + "ItemList": "templateView.baseClass.ItemList" }, "nineSliceTop": "templateView.nineSliceTop", "nineSliceRight": "templateView.nineSliceRight", @@ -1165,7 +1187,13 @@ "isUi": "templateView.isUi", "defaultCount": "templateView.defaultCount", "useBitmapText": "templateView.useBitmapText", - "errorBitmapNotConfigured": "templateView.errorBitmapNotConfigured" + "errorBitmapNotConfigured": "templateView.errorBitmapNotConfigured", + "layoutItemsHeading": "templateView.layoutItemsHeading", + "horizontalPadding": "templateView.horizontalPadding", + "verticalPadding": "templateView.verticalPadding", + "horizontalSpacing": "templateView.horizontalSpacing", + "verticalSpacing": "templateView.verticalSpacing", + "alignItems": "templateView.alignItems" }, "assetInput": { "changeAsset": "assetInput.changeAsset", @@ -1403,7 +1431,6 @@ "settings": "catnip.coreLibs.settings", "sounds": "catnip.coreLibs.sounds", "strings": "catnip.coreLibs.strings", - "styles": "catnip.coreLibs.styles", "templates": "catnip.coreLibs.templates", "utilities": "catnip.coreLibs.utilities", "actions": "catnip.coreLibs.actions", @@ -1693,7 +1720,14 @@ "delete from storage": "catnip.blockNames.delete from storage", "load from storage": "catnip.blockNames.load from storage", "is key in storage": "catnip.blockNames.is key in storage", - "owning room": "catnip.blockNames.owning room" + "owning room": "catnip.blockNames.owning room", + "x prev": "catnip.blockNames.x prev", + "y prev": "catnip.blockNames.y prev", + "content type entries": "catnip.blockNames.content type entries", + "action value": "catnip.blockNames.action value", + "is action pressed": "catnip.blockNames.is action pressed", + "is action down": "catnip.blockNames.is action down", + "is action released": "catnip.blockNames.is action released" }, "blockDisplayNames": { "write": "catnip.blockDisplayNames.write", @@ -1781,10 +1815,22 @@ "blockDocumentation": { "serialize object": "catnip.blockDocumentation.serialize object", "constant string": "catnip.blockDocumentation.constant string" - } + }, + "globalVariables": "catnip.globalVariables", + "globalVariablesHint": "catnip.globalVariablesHint", + "createNewGlobalVariable": "catnip.createNewGlobalVariable", + "newGlobalVariablePrompt": "catnip.newGlobalVariablePrompt", + "invalidVarNameError": "catnip.invalidVarNameError", + "renamePropertyPrompt": "catnip.renamePropertyPrompt", + "renameVariablePrompt": "catnip.renameVariablePrompt", + "renamingAcrossProject": "catnip.renamingAcrossProject" }, "regionalLinks": { "discord": "regionalLinks.discord", "telegram": "regionalLinks.telegram" + }, + "enumEditor": { + "addVariant": "enumEditor.addVariant", + "enumUseCases": "enumEditor.enumUseCases" } -} \ No newline at end of file +} From 1de688dc0a726be34eee7a805a6e45242de3531e Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 11 Jul 2024 21:19:02 +1200 Subject: [PATCH 23/44] :bug: Fix not being able to port v3 versions to v5 (fixes incorrect sound conversion) --- .../projectMigrationScripts/4.0.0-next-3.js | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/js/projectMigrationScripts/4.0.0-next-3.js b/src/js/projectMigrationScripts/4.0.0-next-3.js index 0f995d150..7630e2e51 100644 --- a/src/js/projectMigrationScripts/4.0.0-next-3.js +++ b/src/js/projectMigrationScripts/4.0.0-next-3.js @@ -2,10 +2,9 @@ window.migrationProcess = window.migrationProcess || []; window.migrationProcess.push({ version: '4.0.0-next-3', - process: project => new Promise(resolve => { + process: async project => { const toPatchScriptables = []; const toPatchSounds = []; - const async = []; const walker = collection => { for (const item of collection) { if (['room', 'template'].includes(item.type)) { @@ -37,32 +36,24 @@ window.migrationProcess.push({ const path = require('path'); const fs = require('fs-extra'); const {createAsset, addSoundFile} = require('src/node_requires/resources/sounds'); - for (const sound of toPatchSounds) { + await Promise.all(toPatchSounds.map(async sound => { if (!sound.origname) { - continue; + return; } const oldFile = window.projdir + '/snd/s' + sound.uid + path.extname(sound.origname); const preload = sound.isMusic; - for (const oldKey of ['origname', 'ogg', 'mp3', 'wav', 'poolSize', 'isMusic']) { - delete sound[oldKey]; - } - const newSound = createAsset(); - delete newSound.name; - delete newSound.uid; + const newSound = await createAsset(sound.name); newSound.preload = preload; + await addSoundFile(newSound, oldFile); + for (const key of ['isMusic', 'origname', 'wav', 'ogg', 'mp3', 'poolSize']) { + delete sound[key]; + } Object.assign(sound, newSound); - async.push(addSoundFile(sound, oldFile) - .then(() => fs.remove(oldFile))); - } - } - if (async.length > 0) { - Promise.all(async) - .then(() => resolve()); - } else { - resolve(); + await fs.remove(oldFile); + })); } - }) + } }); From 3e94ddcf2f5bbda27262c6bb3d53fc4b4a605b9f Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 12 Jul 2024 21:02:57 +1200 Subject: [PATCH 24/44] :zap: Disable Vulkan support by default due to frequent issues with it on Linux --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index f4d17d196..c35a7fb27 100644 --- a/app/package.json +++ b/app/package.json @@ -50,7 +50,7 @@ "webkit": { "plugin": false }, - "chromium-args": "--enable-features=nw2 --load-extensions --force-color-profile=srgb --disable-features=ColorCorrectRendering", + "chromium-args": "--enable-features=nw2 --load-extensions --force-color-profile=srgb --disable-features=ColorCorrectRendering --disable-features=Vulkan", "dependencies": { "@neutralinojs/neu": "^11.0.1", "png2icons": "^2.0.1", From 85b8a849315126ff73a209db01ce2d58433c2f39 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 18:40:41 +1200 Subject: [PATCH 25/44] :sparkles: Paste textures with Ctrl+V while on the Assets tab --- src/node_requires/hotkeys.js | 6 ++++++ src/riotTags/app-view.tag | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/node_requires/hotkeys.js b/src/node_requires/hotkeys.js index c6a06446e..52316bb3e 100644 --- a/src/node_requires/hotkeys.js +++ b/src/node_requires/hotkeys.js @@ -181,12 +181,15 @@ class Hotkeys { this.scopeStack = val.split(' '); } } + /** Adds a scope key. */ push(val) { this.scopeStack.push(val); } + /** Removes the lastly pushed scope key. */ pop() { return this.scopeStack.pop(); } + /** Remove the given scope key. Usually you would want to use .exit instead. */ remove(val) { const ind = this.scopeStack.indexOf(val); if (val !== -1) { @@ -194,6 +197,7 @@ class Hotkeys { } return ind !== -1; } + /** Truncates the scope to remove the given scope and all its children. */ exit(val) { const ind = this.scopeStack.indexOf(val); if (val !== -1) { @@ -201,9 +205,11 @@ class Hotkeys { } return ind !== -1; } + /** Completely resets the hotkey scope */ cleanScope() { this.scopeStack.length = 0; } + /** Returns whether the current state is in the given hotkey scope */ inScope(val) { return this.scopeStack.indexOf(val) !== -1; } diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag index 692950e7b..e6d44651c 100644 --- a/src/riotTags/app-view.tag +++ b/src/riotTags/app-view.tag @@ -127,6 +127,8 @@ app-view.flexcol this.mixin(require('src/node_requires/riotMixins/voc').default); this.tab = 'assets'; // A tab can be either a string ('project', 'assets', etc.) or an asset object + window.hotkeys.cleanScope(); + window.hotkeys.push('assets'); this.openedAssets = []; this.tabsDirty = []; const webglUsers = ['style', 'tandem', 'room']; @@ -167,6 +169,7 @@ app-view.flexcol window.hotkeys.push(tab.uid); } }; + window.signals.on('assetChanged', this.refreshDirty); this.on('unmount', () => { window.signals.off('assetChanged', this.refreshDirty); @@ -552,6 +555,33 @@ app-view.flexcol this.scrollableRight = val < tabswrap.scrollWidth - tabswrap.clientWidth; }; + // Paste handler for pasting textures + this.tryPasteAssets = async () => { + if (!window.hotkeys.inScope('assets')) { + return; + } + // Try to load a texture + const png = nw.Clipboard.get().get('png'); + if (!png) { + alertify.error(this.vocGlob.couldNotLoadFromClipboard); + return; + } + const imageBase64 = png.replace(/^data:image\/\w+;base64,/, ''); + const imageBuffer = new Buffer(imageBase64, 'base64'); + + const {createAsset} = require('src/node_requires/resources'); + await createAsset('texture', this.refs.assets.currentFolder, { + src: imageBuffer + }); + + alertify.success(this.vocGlob.pastedFromClipboard); + this.refs.assets.update(); + }; + window.hotkeys.on('Control+v', this.tryPasteAssets); + this.on('unmount', () => { + window.hotkeys.off('Control+v', this.tryPasteAssets); + }); + this.toggleFullscreen = function toggleFullscreen() { nw.Window.get().toggleFullscreen(); }; From 2853aa52e74b9af8fae567b17134831f86e3b556 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 18:45:32 +1200 Subject: [PATCH 26/44] :bug: Importing a texture from a Buffer must prompt a user for a texture name --- src/node_requires/resources/textures/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/node_requires/resources/textures/index.ts b/src/node_requires/resources/textures/index.ts index 4b0294866..53cee9fbb 100644 --- a/src/node_requires/resources/textures/index.ts +++ b/src/node_requires/resources/textures/index.ts @@ -1,6 +1,7 @@ import {uidMap, getOfType, getById, createAsset, IAssetContextItem} from '..'; import {TexturePreviewer} from '../preview/texture'; import {convertToPng} from '../../utils/imageUtils'; +import {promptName} from '../promptName'; import fs from 'fs-extra'; import path from 'path'; @@ -280,7 +281,12 @@ const importImageToTexture = async (opts: { if (opts.name) { texName = opts.name; } else if (opts.src instanceof Buffer) { - texName = 'NewTexture'; + const name = await promptName('texture', 'NewTexture'); + if (name) { + texName = name; + } else { + texName = 'NewTexture_' + id.slice(-4); + } } else { texName = path.basename(opts.src) .replace(/\.(jpg|gif|png|jpeg)/gi, '') @@ -510,7 +516,7 @@ export const assetContextMenuItems: IAssetContextItem[] = [{ const createTexture = async (payload?: Parameters[0]): Promise => { if (payload) { - return importImageToTexture(payload as Parameters[0]); + return importImageToTexture(payload); } const inputPath = await window.showOpenDialog({ filter: '.png,.jpg,.jpeg,.bmp,.tiff,.webp' From 9dea087b094e75ab7d6f1c4651d5b9e65600e284 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 18:53:32 +1200 Subject: [PATCH 27/44] :zap: Widen the asset confirmation dialog a bit --- src/styl/tags/app-view.styl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/styl/tags/app-view.styl b/src/styl/tags/app-view.styl index 87294040b..80ecd54d7 100644 --- a/src/styl/tags/app-view.styl +++ b/src/styl/tags/app-view.styl @@ -83,6 +83,8 @@ app-view display inline-block .&-anAssetConfirmDialog.aModal width 45rem + asset-confirm .aModal + width 35rem @media (max-width 1100px) app-view > nav > .tabs > li From 63e16aa6dd46502d3472afb3d70b8dae2c0d2dac Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 19:10:14 +1200 Subject: [PATCH 28/44] :zap: Use UI theme colors in room editor's tile picker --- src/riotTags/editors/room-editor/room-tile-editor.tag | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/riotTags/editors/room-editor/room-tile-editor.tag b/src/riotTags/editors/room-editor/room-tile-editor.tag index a243ecb6a..cce27a467 100644 --- a/src/riotTags/editors/room-editor/room-tile-editor.tag +++ b/src/riotTags/editors/room-editor/room-tile-editor.tag @@ -74,6 +74,8 @@ room-tile-editor.room-editor-Tiles.flexfix(class="{opts.class}") this.mixin(require('src/node_requires/riotMixins/voc').default); this.mixin(require('src/node_requires/riotMixins/wire').default); + const {getSwatch} = require('src/node_requires/themes'); + this.on('update', () => { if (!this.opts.layer && this.opts.layers.length) { this.opts.onchangelayer(this.opts.layers[0]); @@ -183,7 +185,7 @@ room-tile-editor.room-editor-Tiles.flexfix(class="{opts.class}") } cx.globalAlpha = 1; cx.drawImage(img, 0, 0); - cx.strokeStyle = '#0ff'; + cx.strokeStyle = getSwatch('accent1'); cx.lineWidth = 1; cx.globalAlpha = 0.5; cx.globalCompositeOperation = 'exclusion'; @@ -199,14 +201,14 @@ room-tile-editor.room-editor-Tiles.flexfix(class="{opts.class}") cx.globalCompositeOperation = 'source-over'; cx.globalAlpha = 1; cx.lineJoin = 'round'; - cx.strokeStyle = localStorage.UItheme === 'Night' ? '#44dbb5' : '#446adb'; + cx.strokeStyle = getSwatch('act'); cx.lineWidth = 3; const selX = tex.offx + this.tileX * (tex.width + tex.marginx), selY = tex.offy + this.tileY * (tex.height + tex.marginy), selW = tex.width * this.tileSpanX + tex.marginx * (this.tileSpanX - 1), selH = tex.height * this.tileSpanY + tex.marginy * (this.tileSpanY - 1); cx.strokeRect(-0.5 + selX, -0.5 + selY, selW + 1, selH + 1); - cx.strokeStyle = localStorage.UItheme === 'Night' ? '#1C2B42' : '#fff'; + cx.strokeStyle = getSwatch('background'); cx.lineWidth = 1; cx.strokeRect(-0.5 + selX, -0.5 + selY, selW + 1, selH + 1); }; From 686212201e1452fa07f87321aa8b3fd4981ae2f7 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 19:17:48 +1200 Subject: [PATCH 29/44] :zap: Remember last used tileset in an edited room --- src/node_requires/resources/rooms/IRoom.d.ts | 1 + .../editors/room-editor/room-editor.tag | 1 + .../editors/room-editor/room-tile-editor.tag | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/node_requires/resources/rooms/IRoom.d.ts b/src/node_requires/resources/rooms/IRoom.d.ts index e054dba0d..a4833217b 100644 --- a/src/node_requires/resources/rooms/IRoom.d.ts +++ b/src/node_requires/resources/rooms/IRoom.d.ts @@ -109,6 +109,7 @@ interface IRoom extends IScriptableBehaviors { gridY: number; diagonalGrid: boolean; disableGrid: boolean; + lastPickedTileset?: assetRef; simulate: boolean; restrictCamera?: boolean; restrictMinX?: number; diff --git a/src/riotTags/editors/room-editor/room-editor.tag b/src/riotTags/editors/room-editor/room-editor.tag index cf0f3d628..041f94ef6 100644 --- a/src/riotTags/editors/room-editor/room-editor.tag +++ b/src/riotTags/editors/room-editor/room-editor.tag @@ -94,6 +94,7 @@ room-editor.aPanel.aView(data-hotkey-scope="{asset.uid}") ) room-tile-editor.room-editor-aContextPanel( if="{currentTool === 'addTiles'}" + room="{asset}" layer="{currentTileLayer}" layers="{pixiEditor.tileLayers}" onchangetile="{changeTilePatch}" diff --git a/src/riotTags/editors/room-editor/room-tile-editor.tag b/src/riotTags/editors/room-editor/room-tile-editor.tag index cce27a467..915235dd9 100644 --- a/src/riotTags/editors/room-editor/room-tile-editor.tag +++ b/src/riotTags/editors/room-editor/room-tile-editor.tag @@ -5,6 +5,8 @@ All exising tile layers in the current room. @attribute pixieditor (RoomEditor) When other attributes are not enough + @attribute room (IRoom) + Currently edited room. Used to save last used tileset texture. @attribute onchangetile (riot function) Called with ITileSelection instance as its only argument @@ -157,11 +159,13 @@ room-tile-editor.room-editor-Tiles.flexfix(class="{opts.class}") this.pickingTileset = false; this.update(); }; + const {getById} = require('src/node_requires/resources'); this.onTilesetSelected = async textureId => { - const {getById} = require('src/node_requires/resources'); const {getDOMImageFromTexture} = require('src/node_requires/resources/textures'); this.currentTexture = getById('texture', textureId); this.pickingTileset = false; + // Remember the ID to automatically load it later + this.opts.room.lastPickedTileset = textureId; this.update(); this.currentTextureImg = await getDOMImageFromTexture(this.currentTexture); this.redrawTileset(); @@ -171,6 +175,18 @@ room-tile-editor.room-editor-Tiles.flexfix(class="{opts.class}") this.update(); }; + // Automatically pick a previously used in this room texture + if (this.opts.room.lastPickedTileset && this.opts.room.lastPickedTileset !== -1) { + const id = this.opts.room.lastPickedTileset; + try { + // Make sure a texture exists + getById('texture', id); + this.onTilesetSelected(id); + } catch (oO) { + void oO; + } + } + this.redrawTileset = () => { var c = this.refs.tiledImage, cx = c.getContext('2d'), From 8ea45b1d94474f8bb8ac66e99d291e09fd6c4ed0 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 19:22:34 +1200 Subject: [PATCH 30/44] :zap: Allow closing most success/error/warning messages in the bottom-right corner with a click --- src/js/3rdparty/alertify.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/3rdparty/alertify.js b/src/js/3rdparty/alertify.js index d648227e6..1f832654e 100644 --- a/src/js/3rdparty/alertify.js +++ b/src/js/3rdparty/alertify.js @@ -169,7 +169,6 @@ } } } - this.notify(message, type, click); }, @@ -219,6 +218,10 @@ // Add the click handler, if specified. if (typeof click === 'function') { log.addEventListener('click', click); + } else { + log.addEventListener('click', () => { + this.close(log, -1); + }); } elLog.appendChild(log); From 901ef4dd4158b1c5222e73ba50f5bc968d7ed084 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 20:14:13 +1200 Subject: [PATCH 31/44] :sparkles: Room editor: show a counter when placing copies or tiles in a straight line (with a Shift key) --- src/node_requires/roomEditor/index.ts | 29 +++++++++++++++---- .../interactions/copies/placeCopy.ts | 2 ++ .../interactions/placementCalculator.ts | 21 ++++++++++++++ .../interactions/tiles/placeTile.ts | 2 ++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/node_requires/roomEditor/index.ts b/src/node_requires/roomEditor/index.ts index 38e1e1ee5..307db8a04 100644 --- a/src/node_requires/roomEditor/index.ts +++ b/src/node_requires/roomEditor/index.ts @@ -85,7 +85,10 @@ class RoomEditor extends PIXI.Application { marqueeBox = new MarqueeBox(this, 0, 0, 10, 10); /** A container for all the room's entities */ room = new PIXI.Container(); - /** A container for viewport boxes, grid, and other overlays */ + /** + * A container for viewport boxes, grid, and other overlays. + * It is positioned to be in room coordinates. + */ overlays = new PIXI.Container(); /** * A Graphics instance used to draw selection frames on top of entities, @@ -104,6 +107,14 @@ class RoomEditor extends PIXI.Application { * in a left-bottom corner. Useful for lining up things in a level. */ pointerCoords = new PIXI.Text('(0;0)', defaultTextStyle); + /** + * A label used to display the amount of copies or tiles a user will place in a room. + * Used with Shift+drag placement operations. + */ + ghostCounter = new PIXI.Text('1', { + ...defaultTextStyle, + fontSize: 21 + }); /** * A label that will follow mouse cursor and display entities' relevant names * like template name, used sound asset, etc. @@ -168,13 +179,19 @@ class RoomEditor extends PIXI.Application { this.stage.addChild(this.room); this.redrawGrid(); - this.overlays.addChild(this.grid); - this.stage.addChild(this.overlays); this.compoundGhost.alpha = 0.5; - this.overlays.addChild(this.compoundGhost); this.marqueeBox.visible = false; - this.overlays.addChild(this.marqueeBox); - this.overlays.addChild(this.snapTarget); + this.ghostCounter.zIndex = Infinity; + this.ghostCounter.visible = false; + this.ghostCounter.anchor.set(0.5, 0.5); + this.overlays.addChild( + this.grid, + this.compoundGhost, + this.marqueeBox, + this.snapTarget, + this.ghostCounter + ); + this.stage.addChild(this.overlays); this.deserialize(editor.room as IRoom); this.stage.addChild(this.selectionOverlay); this.stage.addChild(this.transformer); diff --git a/src/node_requires/roomEditor/interactions/copies/placeCopy.ts b/src/node_requires/roomEditor/interactions/copies/placeCopy.ts index a1ca81f5d..99c271403 100644 --- a/src/node_requires/roomEditor/interactions/copies/placeCopy.ts +++ b/src/node_requires/roomEditor/interactions/copies/placeCopy.ts @@ -88,6 +88,7 @@ export const placeCopy: IRoomEditorInteraction = { const newPos = this.snapTarget.position.clone(); const ghosts = calcPlacement( newPos, + this, affixedData, ((position): Copy => { soundbox.play('Wood_Start'); @@ -139,6 +140,7 @@ export const placeCopy: IRoomEditorInteraction = { } } soundbox.play('Wood_End'); + this.ghostCounter.visible = false; this.compoundGhost.removeChildren(); this.history.pushChange({ type: 'creation', diff --git a/src/node_requires/roomEditor/interactions/placementCalculator.ts b/src/node_requires/roomEditor/interactions/placementCalculator.ts index e1fa07315..4f07cb821 100644 --- a/src/node_requires/roomEditor/interactions/placementCalculator.ts +++ b/src/node_requires/roomEditor/interactions/placementCalculator.ts @@ -11,6 +11,7 @@ when a new object should be placed. */ import {fromRectangular, fromDiagonal, toRectangular, toDiagonal} from '../common'; +import {RoomEditor} from '..'; import * as PIXI from 'pixi.js'; @@ -39,6 +40,7 @@ type ISimplePoint = { // eslint-disable-next-line max-lines-per-function export const calcPlacement = ( newPos: PIXI.IPoint, + editor: RoomEditor, affixedData: PlacementData, placeImmediately: (position: PIXI.IPoint) => void ): ghostPoints => { @@ -134,5 +136,24 @@ export const calcPlacement = ( }, affixedData.gridX, affixedData.gridY); ghosts.push(localPos); } + + // Display a copy counter when placing items in a straight line + editor.ghostCounter.visible = true; + const count = ghosts.length + 1; + if (editor.ghostCounter.text !== count.toString()) { + editor.ghostCounter.text = count; + } + if (ghosts.length > 0) { + const [firstGhost] = ghosts, + lastGhost = ghosts[ghosts.length - 1]; + editor.ghostCounter.position.set( + (firstGhost.x + lastGhost.x) / 2, + (firstGhost.y + lastGhost.y) / 2 + ); + } else { + editor.ghostCounter.position.set(affixedData.startPos.x, affixedData.startPos.y); + } + editor.ghostCounter.scale.set(editor.camera.scale.x, editor.camera.scale.y); + return ghosts; }; diff --git a/src/node_requires/roomEditor/interactions/tiles/placeTile.ts b/src/node_requires/roomEditor/interactions/tiles/placeTile.ts index 7d71bf196..96fda2ae8 100644 --- a/src/node_requires/roomEditor/interactions/tiles/placeTile.ts +++ b/src/node_requires/roomEditor/interactions/tiles/placeTile.ts @@ -129,6 +129,7 @@ export const placeTile: IRoomEditorInteraction = { const newPos = this.snapTarget.position.clone(); const ghosts = calcPlacement( newPos, + this, affixedData, (position => { soundbox.play('Wood_Start'); @@ -185,6 +186,7 @@ export const placeTile: IRoomEditorInteraction = { } } soundbox.play('Wood_End'); + this.ghostCounter.visible = false; this.compoundGhost.removeChildren(); this.stage.eventMode = 'static'; // Causes to rediscover nested elements (is it relevant for v7?) if (affixedData.created.size) { From 57e3820961e1f41e060a51019a112c0e908d6b30 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 20:41:38 +1200 Subject: [PATCH 32/44] :sparkles: Place filled rectangles of copies or tiles with Shift+Ctrl modifier in a room editor --- .../interactions/copies/deleteCopies.ts | 2 +- .../interactions/copies/placeCopy.ts | 15 +- .../interactions/placementCalculator.ts | 140 +++++++++++------- .../interactions/tiles/deleteTiles.ts | 2 +- .../interactions/tiles/placeTile.ts | 19 ++- .../editors/room-editor/room-editor.tag | 8 +- 6 files changed, 116 insertions(+), 70 deletions(-) diff --git a/src/node_requires/roomEditor/interactions/copies/deleteCopies.ts b/src/node_requires/roomEditor/interactions/copies/deleteCopies.ts index 25810283c..e81c81719 100644 --- a/src/node_requires/roomEditor/interactions/copies/deleteCopies.ts +++ b/src/node_requires/roomEditor/interactions/copies/deleteCopies.ts @@ -13,7 +13,7 @@ export const deleteCopies: IRoomEditorInteraction = { if (this.riotEditor.currentTool !== 'addCopies') { return false; } - return e.button === 0 && (e.ctrlKey || e.metaKey); + return e.button === 0 && (e.ctrlKey || e.metaKey) && !e.shiftKey; }, listeners: { pointerdown(e: PIXI.FederatedPointerEvent, riotTag, affixedData) { diff --git a/src/node_requires/roomEditor/interactions/copies/placeCopy.ts b/src/node_requires/roomEditor/interactions/copies/placeCopy.ts index 99c271403..1c8d804d6 100644 --- a/src/node_requires/roomEditor/interactions/copies/placeCopy.ts +++ b/src/node_requires/roomEditor/interactions/copies/placeCopy.ts @@ -8,7 +8,7 @@ import {soundbox} from '../../../3rdparty/soundbox'; import * as PIXI from 'pixi.js'; interface IAffixedData { - mode: 'free' | 'straight'; + mode: 'free' | 'straight' | 'rect'; startPos: PIXI.IPoint; prevPos: PIXI.IPoint; prevLength: number; @@ -57,13 +57,18 @@ export const placeCopy: IRoomEditorInteraction = { pointerdown(e: PIXI.FederatedPointerEvent, roomTag, affixedData) { this.compoundGhost.removeChildren(); affixedData.created = new Set(); - // Two possible modes: placing in straight vertical/horizontal/diagonal lines + // Tree possible modes: placing in straight vertical/horizontal/diagonal lines, + // filling in a rectangle (shift + ctrl keys), // and in a free form, like drawing with a brush. // Straight method creates a ghost preview before actually creating all the copies, // while the free form places copies as a user moves their cursor. if (e.shiftKey) { - affixedData.mode = 'straight'; - affixedData.prevLength = 1; + if (e.ctrlKey) { + affixedData.mode = 'rect'; + } else { + affixedData.mode = 'straight'; + affixedData.prevLength = 1; + } } else { affixedData.mode = 'free'; } @@ -127,7 +132,7 @@ export const placeCopy: IRoomEditorInteraction = { } }, pointerup(e, roomTag, affixedData, callback) { - if (affixedData.mode === 'straight') { + if (affixedData.mode === 'straight' || affixedData.mode === 'rect') { // Replace all the preview copies with real ones for (const ghost of this.compoundGhost.children) { const copy = createCopy( diff --git a/src/node_requires/roomEditor/interactions/placementCalculator.ts b/src/node_requires/roomEditor/interactions/placementCalculator.ts index 4f07cb821..dc01b7333 100644 --- a/src/node_requires/roomEditor/interactions/placementCalculator.ts +++ b/src/node_requires/roomEditor/interactions/placementCalculator.ts @@ -16,7 +16,7 @@ import {RoomEditor} from '..'; import * as PIXI from 'pixi.js'; type PlacementData = { - mode: 'free' | 'straight'; + mode: 'free' | 'straight' | 'rect'; startPos: PIXI.IPoint; prevPos: PIXI.IPoint; prevLength: number; @@ -84,8 +84,6 @@ export const calcPlacement = ( } affixedData.prevPos = newPos; - - // Straight-line placement const startGrid = to( affixedData.startPos, affixedData.gridX, @@ -98,62 +96,94 @@ export const calcPlacement = ( ); const dx = Math.abs(startGrid.x - endGrid.x), dy = Math.abs(startGrid.y - endGrid.y); - const straightEndGrid: ISimplePoint = { - x: 0, - y: 0 - }; - const angle = Math.atan2(dy, dx); - if (Math.abs(angle) > Math.PI * 0.375 && Math.abs(angle) < Math.PI * 0.525) { - // Seems to be a vertical line - straightEndGrid.x = startGrid.x; - straightEndGrid.y = endGrid.y; - } else if (Math.abs(angle) < Math.PI * 0.125) { - // Seems to be a horizontal line - straightEndGrid.x = endGrid.x; - straightEndGrid.y = startGrid.y; - } else { - // It is more or so diagonal - const max = Math.max(dx, dy); - straightEndGrid.x = endGrid.x > startGrid.x ? - startGrid.x + max : - startGrid.x - max; - straightEndGrid.y = endGrid.y > startGrid.y ? - startGrid.y + max : - startGrid.y - max; + + // Display a copy counter when placing items in a straight line or in a rectangle + editor.ghostCounter.visible = true; + editor.ghostCounter.scale.set(editor.camera.scale.x, editor.camera.scale.y); + + // Straight-line placement + if (affixedData.mode === 'straight') { + const straightEndGrid: ISimplePoint = { + x: 0, + y: 0 + }; + const angle = Math.atan2(dy, dx); + if (Math.abs(angle) > Math.PI * 0.375 && Math.abs(angle) < Math.PI * 0.525) { + // Seems to be a vertical line + straightEndGrid.x = startGrid.x; + straightEndGrid.y = endGrid.y; + } else if (Math.abs(angle) < Math.PI * 0.125) { + // Seems to be a horizontal line + straightEndGrid.x = endGrid.x; + straightEndGrid.y = startGrid.y; + } else { + // It is more or so diagonal + const max = Math.max(dx, dy); + straightEndGrid.x = endGrid.x > startGrid.x ? + startGrid.x + max : + startGrid.x - max; + straightEndGrid.y = endGrid.y > startGrid.y ? + startGrid.y + max : + startGrid.y - max; + } + const incX = Math.sign(straightEndGrid.x - startGrid.x) * affixedData.stepX, + incY = Math.sign(straightEndGrid.y - startGrid.y) * affixedData.stepY; + const l = Math.max(dx / affixedData.stepX, dy / affixedData.stepY); + const ghosts = []; + // Calculate ghost positions + for (let i = 0, {x, y} = startGrid; + i < l; + i++, x += incX, y += incY + ) { + const localPos = from({ + x: x + incX, + y: y + incY + }, affixedData.gridX, affixedData.gridY); + ghosts.push(localPos); + } + + const count = ghosts.length + 1; + if (editor.ghostCounter.text !== count.toString()) { + editor.ghostCounter.text = count; + } + if (ghosts.length > 0) { + const [firstGhost] = ghosts, + lastGhost = ghosts[ghosts.length - 1]; + editor.ghostCounter.position.set( + (firstGhost.x + lastGhost.x) / 2, + (firstGhost.y + lastGhost.y) / 2 + ); + } else { + editor.ghostCounter.position.set(affixedData.startPos.x, affixedData.startPos.y); + } + + return ghosts; } - const incX = Math.sign(straightEndGrid.x - startGrid.x) * affixedData.stepX, - incY = Math.sign(straightEndGrid.y - startGrid.y) * affixedData.stepY; - const l = Math.max(dx / affixedData.stepX, dy / affixedData.stepY); + + // Rectangle fill mode + const left = Math.min(startGrid.x, endGrid.x), + top = Math.min(startGrid.y, endGrid.y), + right = Math.max(startGrid.x, endGrid.x), + bottom = Math.max(startGrid.y, endGrid.y); const ghosts = []; - // Calculate ghost positions - for (let i = 0, {x, y} = startGrid; - i < l; - i++, x += incX, y += incY - ) { - const localPos = from({ - x: x + incX, - y: y + incY - }, affixedData.gridX, affixedData.gridY); - ghosts.push(localPos); + for (let x = left; x <= right; x++) { + for (let y = top; y <= bottom; y++) { + const localPos = from({ + x, + y + }, affixedData.gridX, affixedData.gridY); + ghosts.push(localPos); + } } - - // Display a copy counter when placing items in a straight line editor.ghostCounter.visible = true; - const count = ghosts.length + 1; - if (editor.ghostCounter.text !== count.toString()) { - editor.ghostCounter.text = count; - } - if (ghosts.length > 0) { - const [firstGhost] = ghosts, - lastGhost = ghosts[ghosts.length - 1]; - editor.ghostCounter.position.set( - (firstGhost.x + lastGhost.x) / 2, - (firstGhost.y + lastGhost.y) / 2 - ); - } else { - editor.ghostCounter.position.set(affixedData.startPos.x, affixedData.startPos.y); + const text = `${right - left + 1}×${bottom - top + 1}`; + if (editor.ghostCounter.text !== text) { + editor.ghostCounter.text = text; } - editor.ghostCounter.scale.set(editor.camera.scale.x, editor.camera.scale.y); - + const globalCenter = from({ + x: (left + right) / 2, + y: (top + bottom) / 2 + }, affixedData.gridX, affixedData.gridY); + editor.ghostCounter.position.set(globalCenter.x, globalCenter.y); return ghosts; }; diff --git a/src/node_requires/roomEditor/interactions/tiles/deleteTiles.ts b/src/node_requires/roomEditor/interactions/tiles/deleteTiles.ts index 7dbecf491..73e786cc4 100644 --- a/src/node_requires/roomEditor/interactions/tiles/deleteTiles.ts +++ b/src/node_requires/roomEditor/interactions/tiles/deleteTiles.ts @@ -14,7 +14,7 @@ export const deleteTiles: IRoomEditorInteraction = { if (this.riotEditor.currentTool !== 'addTiles' || !this.riotEditor.currentTileLayer) { return false; } - return e.button === 0 && (e.ctrlKey || e.metaKey); + return e.button === 0 && (e.ctrlKey || e.metaKey) && !e.shiftKey; }, listeners: { pointerdown(e: PIXI.FederatedPointerEvent, riotTag, affixedData) { diff --git a/src/node_requires/roomEditor/interactions/tiles/placeTile.ts b/src/node_requires/roomEditor/interactions/tiles/placeTile.ts index 96fda2ae8..f8dd6e606 100644 --- a/src/node_requires/roomEditor/interactions/tiles/placeTile.ts +++ b/src/node_requires/roomEditor/interactions/tiles/placeTile.ts @@ -11,7 +11,7 @@ import {soundbox} from '../../../3rdparty/soundbox'; import {getLanguageJSON} from '../../../i18n'; interface IAffixedData { - mode: 'free' | 'straight'; + mode: 'free' | 'straight' | 'rect'; startPos: PIXI.IPoint; prevPos: PIXI.IPoint; prevLength: number; @@ -86,16 +86,21 @@ export const placeTile: IRoomEditorInteraction = { return Boolean(riotTag.tilePatch?.texture); }, listeners: { - pointerdown(e, riotTag, affixedData) { + pointerdown(e: PIXI.FederatedPointerEvent, riotTag, affixedData) { this.compoundGhost.removeChildren(); affixedData.created = new Set(); - // Two possible modes: placing in straight vertical/horizontal/diagonal lines + // Tree possible modes: placing in straight vertical/horizontal/diagonal lines, + // filling in a rectangle (shift + ctrl keys), // and in a free form, like drawing with a brush. // Straight method creates a ghost preview before actually creating all the copies, // while the free form places copies as a user moves their cursor. - if ((e as PIXI.FederatedPointerEvent).shiftKey) { - affixedData.mode = 'straight'; - affixedData.prevLength = 1; + if (e.shiftKey) { + if (e.ctrlKey) { + affixedData.mode = 'rect'; + } else { + affixedData.mode = 'straight'; + affixedData.prevLength = 1; + } } else { affixedData.mode = 'free'; } @@ -171,7 +176,7 @@ export const placeTile: IRoomEditorInteraction = { } }, pointerup(e, riotTag, affixedData, callback) { - if (affixedData.mode === 'straight') { + if (affixedData.mode === 'straight' || affixedData.mode === 'rect') { // Replace all the preview copies with real ones for (const ghost of this.compoundGhost.children) { const newTiles = createTilePatch( diff --git a/src/riotTags/editors/room-editor/room-editor.tag b/src/riotTags/editors/room-editor/room-editor.tag index 041f94ef6..3b2e30b20 100644 --- a/src/riotTags/editors/room-editor/room-editor.tag +++ b/src/riotTags/editors/room-editor/room-editor.tag @@ -182,8 +182,14 @@ room-editor.aPanel.aView(data-hotkey-scope="{asset.uid}") this.freePlacementMode = true; e.preventDefault(); } else if (e.key === 'Control' || e.key === 'Meta') { - this.controlMode = true; + if (e.shiftKey) { + this.controlMode = false; + } else { + this.controlMode = true; + } e.preventDefault(); + } else if (e.key === 'Shift') { + this.controlMode = false; } }; const modifiersUpListener = e => { From 9f8d447be01a835b357c7c9fa6a96082671ec0f2 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 20:56:00 +1200 Subject: [PATCH 33/44] :bug: Fix Ctrl+G hotkey in the room editor --- src/riotTags/editors/room-editor/room-editor.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/riotTags/editors/room-editor/room-editor.tag b/src/riotTags/editors/room-editor/room-editor.tag index 3b2e30b20..1e6388486 100644 --- a/src/riotTags/editors/room-editor/room-editor.tag +++ b/src/riotTags/editors/room-editor/room-editor.tag @@ -212,7 +212,7 @@ room-editor.aPanel.aView(data-hotkey-scope="{asset.uid}") this.freePlacementMode = false; }; const gridToggleListener = e => { - if (!window.hotkeys.inScope('rooms') || window.hotkeys.isFormField(e.target)) { + if (!window.hotkeys.inScope(this.asset.uid) || window.hotkeys.isFormField(e.target)) { return; } this.gridOn = !this.gridOn; From 4fbddff6cd5a2cda26182ffd37f0044dac16a241 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Fri, 19 Jul 2024 21:45:55 +1200 Subject: [PATCH 34/44] :sparkles: Pixel-perfect mode for Scrolling Texture base class --- app/data/i18n/English.json | 1 + .../PixiScrollingTexture.ts | 19 +++++++++++++++---- .../exporter/_exporterContracts.ts | 1 + src/node_requires/exporter/templates.ts | 1 + .../resources/templates/ITemplate.d.ts | 1 + .../resources/templates/index.ts | 1 + .../roomEditor/entityClasses/Copy.ts | 16 ++++++++++++++-- src/riotTags/editors/template-editor.tag | 7 +++++++ 8 files changed, 41 insertions(+), 6 deletions(-) diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 508cb1aef..5711abe78 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -1831,6 +1831,7 @@ "scrollSpeedX": "Scroll speed by X:", "scrollSpeedY": "Scroll speed by Y:", "isUi": "Use UI time", + "pixelPerfectScroll": "Pixel-perfect scroll", "defaultCount": "Default sprite count:" } } diff --git a/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts b/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts index 40ec81caa..175f22896 100644 --- a/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts +++ b/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts @@ -19,6 +19,9 @@ export default class PixiScrollingTexture extends PIXI.TilingSprite { isUi: boolean; #baseWidth: number; #baseHeight: number; + pixelPerfect: boolean; + scrollX = 0; + scrollY = 0; shape: textureShape; constructor(t: ExportedTemplate, exts: Record) { if (t.baseClass !== 'RepeatingTexture') { @@ -31,6 +34,7 @@ export default class PixiScrollingTexture extends PIXI.TilingSprite { this.anchor.set(0); this.scrollSpeedX = t.scrollX; this.scrollSpeedY = t.scrollY; + this.pixelPerfect = Boolean(t.pixelPerfect); if ('scaleX' in exts) { this.width = this.#baseWidth * (exts.scaleX as number ?? 1); } @@ -53,11 +57,18 @@ export default class PixiScrollingTexture extends PIXI.TilingSprite { } tick(): void { if (this.isUi) { - this.tilePosition.x += this.scrollSpeedX * uLib.timeUi; - this.tilePosition.y += this.scrollSpeedY * uLib.timeUi; + this.scrollX += this.scrollSpeedX * uLib.timeUi; + this.scrollY += this.scrollSpeedY * uLib.timeUi; } else { - this.tilePosition.x += this.scrollSpeedX * uLib.time; - this.tilePosition.y += this.scrollSpeedY * uLib.time; + this.scrollX += this.scrollSpeedX * uLib.time; + this.scrollY += this.scrollSpeedY * uLib.time; + } + if (this.pixelPerfect) { + this.tilePosition.x = Math.round(this.scrollX); + this.tilePosition.y = Math.round(this.scrollY); + } else { + this.tilePosition.x = this.scrollX; + this.tilePosition.y = this.scrollY; } } } diff --git a/src/node_requires/exporter/_exporterContracts.ts b/src/node_requires/exporter/_exporterContracts.ts index 701eec5a3..f00cd4c2c 100644 --- a/src/node_requires/exporter/_exporterContracts.ts +++ b/src/node_requires/exporter/_exporterContracts.ts @@ -173,6 +173,7 @@ export type ExportedTemplate = { scrollY: number; isUi: boolean; texture: string; + pixelPerfect: boolean; } | { baseClass: 'SpritedCounter'; spriteCount: number; diff --git a/src/node_requires/exporter/templates.ts b/src/node_requires/exporter/templates.ts index 613b139f0..dccf9ed0e 100644 --- a/src/node_requires/exporter/templates.ts +++ b/src/node_requires/exporter/templates.ts @@ -92,6 +92,7 @@ const getBaseClassInfo = (blankTextures: IBlankTexture[], template: ITemplate) = classInfo += ` scrollX: ${template.tilingSettings!.scrollSpeedX}, scrollY: ${template.tilingSettings!.scrollSpeedY}, + pixelPerfect: ${template.tilingSettings!.pixelPerfect}, isUi: ${template.tilingSettings!.isUi},`; } if (hasCapability(bc, 'repeater')) { diff --git a/src/node_requires/resources/templates/ITemplate.d.ts b/src/node_requires/resources/templates/ITemplate.d.ts index 4414cd91e..91ef89f0c 100644 --- a/src/node_requires/resources/templates/ITemplate.d.ts +++ b/src/node_requires/resources/templates/ITemplate.d.ts @@ -27,6 +27,7 @@ interface ITemplate extends IScriptableBehaviors { tilingSettings?: { scrollSpeedX: number; scrollSpeedY: number; + pixelPerfect: boolean; isUi: boolean; }; repeaterSettings?: { diff --git a/src/node_requires/resources/templates/index.ts b/src/node_requires/resources/templates/index.ts index a1b9fed37..e4926c032 100644 --- a/src/node_requires/resources/templates/index.ts +++ b/src/node_requires/resources/templates/index.ts @@ -141,6 +141,7 @@ export const getBaseClassFields = function (baseClass: TemplateBaseClass): Parti out.tilingSettings = { scrollSpeedX: 0, scrollSpeedY: 0, + pixelPerfect: false, isUi: false }; break; diff --git a/src/node_requires/roomEditor/entityClasses/Copy.ts b/src/node_requires/roomEditor/entityClasses/Copy.ts index 52331b140..04afd64d8 100644 --- a/src/node_requires/roomEditor/entityClasses/Copy.ts +++ b/src/node_requires/roomEditor/entityClasses/Copy.ts @@ -32,6 +32,9 @@ class Copy extends PIXI.Container { initialHeight: number; scrollSpeedX: number; scrollSpeedY: number; + scrollX: number; + scrollY: number; + pixelPerfect: boolean; }; customTextSettings?: { @@ -95,8 +98,15 @@ class Copy extends PIXI.Container { this.sprite?.update(delta); } if (this.tilingSprite) { - this.tilingSprite.tilePosition.x += this.tilingSprite.scrollSpeedX * time; - this.tilingSprite.tilePosition.y += this.tilingSprite.scrollSpeedY * time; + this.tilingSprite.scrollX += this.tilingSprite.scrollSpeedX * time; + this.tilingSprite.scrollY += this.tilingSprite.scrollSpeedY * time; + if (this.tilingSprite.pixelPerfect) { + this.tilingSprite.tilePosition.x = Math.round(this.tilingSprite.scrollX); + this.tilingSprite.tilePosition.y = Math.round(this.tilingSprite.scrollY); + } else { + this.tilingSprite.tilePosition.x = this.tilingSprite.scrollX; + this.tilingSprite.tilePosition.y = this.tilingSprite.scrollY; + } } } @@ -313,6 +323,8 @@ class Copy extends PIXI.Container { if (hasCapability(t.baseClass, 'scroller')) { this.tilingSprite.scrollSpeedX = t.tilingSettings!.scrollSpeedX; this.tilingSprite.scrollSpeedY = t.tilingSettings!.scrollSpeedY; + this.tilingSprite.scrollX = this.tilingSprite.scrollY = 0; + this.tilingSprite.pixelPerfect = t.tilingSettings!.pixelPerfect ?? false; } else { this.tilingSprite.scrollSpeedX = 0; this.tilingSprite.scrollSpeedY = 0; diff --git a/src/riotTags/editors/template-editor.tag b/src/riotTags/editors/template-editor.tag index bff2176c2..a0df44290 100644 --- a/src/riotTags/editors/template-editor.tag +++ b/src/riotTags/editors/template-editor.tag @@ -146,6 +146,13 @@ mixin templateProperties onchange="{parent.wire('asset.tilingSettings.scrollSpeedY')}" value="{parent.asset.tilingSettings.scrollSpeedY}" ) + label.checkbox + input( + type="checkbox" + onchange="{parent.wire('asset.tilingSettings.pixelPerfect')}" + checked="{parent.asset.tilingSettings.pixelPerfect}" + ) + b {parent.voc.pixelPerfectScroll} label.checkbox input( type="checkbox" From 57e6285d313a7dc605af2903068f87bb9a01dc6e Mon Sep 17 00:00:00 2001 From: godmar Date: Fri, 19 Jul 2024 23:19:34 -0400 Subject: [PATCH 35/44] :zap: Ignore actions on not-yet loaded sounds; improve migration from v3 to v5 (#532 by @godmar) - sound actions on sounds that haven't been loaded are now ignored - sound.playing returns false for sounds not yet loaded instead of crashing - strip ct from ct.tween - delete deprecated mouse catmod on 4.0.1 migration to prevent crash --- src/ct.release/sounds.ts | 11 +++++++++-- src/js/projectMigrationScripts/4.0.0-next-1.js | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ct.release/sounds.ts b/src/ct.release/sounds.ts index 7e28143cd..29f167081 100644 --- a/src/ct.release/sounds.ts +++ b/src/ct.release/sounds.ts @@ -100,6 +100,8 @@ const randomRange = (min: number, max: number): number => Math.random() * (max - /** * Applies a method onto a sound — regardless whether it is a sound exported from ct.IDE * (with variants) or imported by a user though `res.loadSound`. + * + * Ignore sounds that have not yet been loaded. */ const withSound = (name: string, fn: (sound: Sound, assetName: string) => T): T => { const pixiFind = PIXI.sound.exists(name) && PIXI.sound.find(name); @@ -113,7 +115,9 @@ const withSound = (name: string, fn: (sound: Sound, assetName: string) => T): let lastVal: T; for (const variant of soundMap[name].variants) { const assetName = `${pixiSoundPrefix}${variant.uid}`; - lastVal = fn(pixiSoundInstances[assetName], assetName); + if (assetName in pixiSoundInstances) { + lastVal = fn(pixiSoundInstances[assetName], assetName); + } // ignore sound variants not yet loaded } return lastVal!; } @@ -365,7 +369,10 @@ export const soundsLib = { return pixiSoundInstances[name].isPlaying; } else if (name in soundMap) { for (const variant of soundMap[name].variants) { - if (pixiSoundInstances[`${pixiSoundPrefix}${variant.uid}`].isPlaying) { + const instanceKey = `${pixiSoundPrefix}${variant.uid}`; + if (instanceKey in pixiSoundInstances && + pixiSoundInstances[instanceKey].isPlaying + ) { return true; } } diff --git a/src/js/projectMigrationScripts/4.0.0-next-1.js b/src/js/projectMigrationScripts/4.0.0-next-1.js index 614f0bda3..236db5aef 100644 --- a/src/js/projectMigrationScripts/4.0.0-next-1.js +++ b/src/js/projectMigrationScripts/4.0.0-next-1.js @@ -12,11 +12,12 @@ window.migrationProcess.push({ } delete project.libs.fittoscreen; delete project.libs.touch; + delete project.libs.mouse; delete project.libs['sound.howler']; delete project.libs['sound.basic']; // `ct.` prefix drop - const regex = /ct\.(meta|camera|templates|rooms|actions|inputs|content|backgrounds|styles|res|emitters|tilemaps|timer|u|pixiApp|stage|loop|fittoscreen|assert|capture|cutscene|desktop|eqs|filters|flow|fs|gamedistribution|inherit|gamepad|keyboard|mouse|pointer|nakama|noise|nanoid|place|random|sprite|storage|touch|transition|ulid|vgui|vkeys|yarn)/g; + const regex = /ct\.(meta|camera|templates|rooms|actions|inputs|content|backgrounds|styles|res|emitters|tilemaps|timer|u|pixiApp|stage|loop|fittoscreen|assert|capture|cutscene|desktop|eqs|filters|flow|fs|gamedistribution|inherit|gamepad|keyboard|mouse|pointer|nakama|noise|nanoid|place|random|sprite|storage|touch|transition|tween|ulid|vgui|vkeys|yarn)/g; const regexSound = /ct\.sound/g; const regexDelta = /ct\.delta/g; const regexRoom = /ct\.room/g; From a7dc26c0905cb9b879bedca17d0fb6cbf1e137ff Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 15:24:08 +1200 Subject: [PATCH 36/44] :globe_with_meridians: Update Turkish translation file by @Sarpmanon --- app/data/i18n/Turkish.json | 592 +++++++++++++++++++++++++++++++++++-- 1 file changed, 564 insertions(+), 28 deletions(-) diff --git a/app/data/i18n/Turkish.json b/app/data/i18n/Turkish.json index 2840484d2..2abea216e 100644 --- a/app/data/i18n/Turkish.json +++ b/app/data/i18n/Turkish.json @@ -161,7 +161,9 @@ "bottomRight": "Alt sağ", "fill": "Doldur", "Scale": "Ölçek" - } + }, + "download": "İndir", + "createStyleFromIt": "Onu kullanarak bir tarz oluştur" }, "assetInput": { "changeAsset": "Varlığı değiştirmek için tıkla", @@ -176,7 +178,8 @@ "unwrapFolder": "Klasörü çöz", "confirmDeleteFolder": "Bu klasörü silmek istediğinden emin misin? Kendi içindeki dosyalar da silinecektir.", "confirmUnwrapFolder": "Bu klasörü çözmek istediğine emin misin? Klasörün tüm içeri şu anki klasöre koyulacak.", - "exportBehavior": "Bu davranışı dışarı aktar" + "exportBehavior": "Bu davranışı dışarı aktar", + "exportTandem": "Bu dağıtıcı tandemini dışarı aktar" }, "builtinAssetGallery": { "galleryTip": "Bu, bedava birsürü doku ve sesin galerisi. Tüm görseller CC0 veya WTFPL, veya ct.js'in özel şartları içinde yayınlanmıştır. Ama bunları diğer projelerinizde istediğiniz gibi kullanabilirsiniz.", @@ -299,7 +302,35 @@ "boosty": "Boosty'de ct.js'i destekle!", "sponsoredBy": "$1 tarafından gururlu bir şekilde desteklendi!", "supportedBy": "$1 tarafından desteklendi", - "nothingToShowFiller": "Burada gösterilecek hiçbir şey yok! Şablonları deneyebilir ya da aşağıdan kendi projeni oluşturabilirsin." + "nothingToShowFiller": "Burada gösterilecek hiçbir şey yok! Şablonları deneyebilir ya da aşağıdan kendi projeni oluşturabilirsin.", + "newUserHeader": "Ct.js'e hoş geldin!", + "welcomeHeaders": [ + "Tekrardan hoş geldin, Tumblr sexyman!", + "Tekrardan hoş geldin, Neo!", + "Bu bizim destansı yazılımcımız değil mi? Tekrardan hoş geldin!", + "Seni tekrardan görmek güzel!", + "Tekrardan hoş geldin, süperstar!", + "Sonunda uyandın. Tekrardan hoş geldin!", + "General Kenobi!", + "🖖", + "Selamlar, yolcu!", + "Ohayo, sempai uwu", + "Mrb :D mrb :D mrb :D", + "Seni görmek güzel, lumberfoot.", + "✉️ Baskın: Hackerman korsanları", + "Sihirdar Vadisi'ne hoş geldin!", + "Test chamber 20'ye hoş geldiniz." + ], + "ctDistributions": { + "released": "Ana Dal", + "nightly": "Nightly sürümü 🌚", + "dev": "Kaynaklardan çalışıyor 🤓" + }, + "gamesFromCommunity": "Topluluk Yapımı Oyunlar", + "submitYourOwn": "Kendininkini Gönder", + "learningResources": "Öğrenim Kaynakları", + "authorBy": "1$ ile", + "telegram": "Telegram grup sohbeti" }, "onboarding": { "hoorayHeader": "Vay canına! Bir proje oluşturdun!", @@ -375,9 +406,6 @@ "fieldReadableName": "Okunabilir isim", "fieldReadableNameHint": "İçerik editöründe kullanılan bu ismin okunabilir versiyonu.", "fieldType": "Tür", - "structureTypes": { - "array": "Array" - }, "deleteContentType": "Bu içerik türünü sil", "confirmDeletionMessage": "Bu içerik türünü silmek istediğinden emin misin? Bu geri alınamaz. Ayrıca bu içerik türündeki tüm girişleri de siler.", "gotoEntries": "Girişlere git", @@ -434,7 +462,8 @@ "deleteScript": "Scripti sil", "moveDown": "Aşağı in", "moveUp": "Yukarı çık", - "newScriptComment": "Küçük libraryleri içe aktarmak ve sık fonksiyonları tanımlamak için scriptleri kullan" + "newScriptComment": "Küçük libraryleri içe aktarmak ve sık fonksiyonları tanımlamak için scriptleri kullan", + "scriptsHint": "Burada oluşturulan scriptler oyununun köküne eklenecek ve böylece oyun her başladığında çalışacak. Sadece JavaScript ve TypeScript desteklenir. Burada belirlenin türler ve değişkenler projenin her yerinde kullanılabilir olacak." }, "export": { "heading": "Dışa aktarma ayarları", @@ -451,10 +480,19 @@ "assetTreeNote": "Varlık ağacını oyun içinde res.tree olarak görüntüleyecek biçimde dışarı aktarabilirsin ama aynı zamanda projenin yapısını da gösterir ve dışarı aktarılan projeyi biraz ağırlaştırır.", "exportAssetTree": "Varlık ağacını dışarı aktar", "exportAssetTypes": "Sadece bu varlık türlerini dışarı aktar:", - "autocloseDesktop": "Kullanıcı \"Kapat\" tuşuna bastığında uygulamadan çık." + "autocloseDesktop": "Kullanıcı \"Kapat\" tuşuna bastığında uygulamadan çık.", + "errorReporting": "Hata raporlama", + "showErrors": "Hataları oyun penceresinde göster (şiddetle tavsiye edilir)", + "showErrorsHint": "Ct.js sana yakalanamamış hataları kullanıcının hata mesajlarını kopyalayabileceği ve sonraki alanda belirleyebileceğin rapor linkini inceleyebileceğin özel bir pencerede gösterecek. Bu da oyununu oynayan oyuncuların devtools'u kullanmadan sana hata bildirmelerine olanak sağlayacak.", + "errorsLink": "Hata raporlamak için bağlantılar: (Github)" }, "catmodsSettings": "Catmod ayarları", - "contentTypes": "İçerik türleri" + "contentTypes": "İçerik türleri", + "main": { + "heading": "Ana ayarlar", + "miscHeading": "Diğer ayarlar", + "backups": "Tutulacak yedek dosyaların sayısı:" + } }, "modules": { "author": "Bu catmod'un sahibi", @@ -688,7 +726,10 @@ "disableAcceleration": "Grafik hızlandırmayı devre dışı bırak (yeniden başlatma gerektirir)", "disableBuiltInDebugger": "Yerleşik hata ayıklayıcıyı devre dışı bırak", "postAnIssue": "GitHub'da bir sorunu yayınla…", - "heading": "Sorun giderme" + "heading": "Sorun giderme", + "disableVulkan": "Vulkan desteğini devre dışı bırak", + "disableVulkanSDHint": "SteamDeck ve başka Linux sistemlerdeki \"WebGL desteği yok\" sorunlarını çözer. Etki etmesi için bir yeniden başlatma zorunludur.", + "restartMessage": "Değişiklikleri uygulamak için lütfen uygulamayı yeniden başlat." }, "deploy": { "heading": "Aç", @@ -712,7 +753,11 @@ "HCBlack": "Yüksek-kontrast siyah", "RosePine": "Gül Çamı", "RosePineMoon": "Gül Çamı Ayı(?)", - "RosePineDawn": "Gül Çamı Şafağı" + "RosePineDawn": "Gül Çamı Şafağı", + "GoldenEye": "Altın Göz", + "AlmaSakura": "Alma Sakura", + "Synthwave": "Synthwave '84", + "OneDarkPro": "Bir Karanlık Pro" }, "language": "Dil", "translateToYourLanguage": "ct.js'i çevir!", @@ -728,7 +773,16 @@ "changeDataFolder": "Veri klasörü yolunu ayarla", "forceProductionForDebug": "Hata ayıklamaları dışarı aktarmak için üretim işlemlerini zorla", "prideMode": "Pride modu", - "altTemplateLayout": "Şablon düzenleyicisi için alternatif düzen" + "altTemplateLayout": "Şablon düzenleyicisi için alternatif düzen", + "changeDebuggerLayout": "Hata ayıklayıcının düzenini değiştir", + "debuggerLayout": "Hata ayıklayıcının düzeni", + "debuggerLayouts": { + "split": "düzeni ayır", + "multiwindow": "çoklu pencereler", + "automatic": "otomatik" + }, + "autoapplyOnLaunch": "Oyunun başlangıcında varlıkları uygula", + "scrollableCatnipLibrary": "Catnip için kaydırılabilir blok kütüphanesi" }, "project": { "heading": "Proje", @@ -796,7 +850,9 @@ "applyAssetsExplanation": "Bazı varlıklar uygulanmadı ve değişiklikleri dışarı aktarılmış projeye yansımayacak. Değişiklikleri şimdi uygulamak ister misin?", "unsavedAssets": "Kaydedilmemiş varlıklar:", "runWithoutApplying": "Yine de başlat", - "applyAndRun": "Uygula ve çalıştır" + "applyAndRun": "Uygula ve çalıştır", + "cantAddEditor": "Cannot add another editor. Please close some tabs with room, style, or tandem editors.", + "loadingPreviouslyOpened": "Önceden açılmış varlıklar yükleniyor..." }, "roomBackgrounds": { "add": "Arkaplan ekle", @@ -816,7 +872,9 @@ "hide": "Katmanı gizle", "findTileset": "Bir Döşeme Bul", "addTileLayer": "Bir döşeme katmanı ekle", - "addTileLayerFirst": "Öncelikle sol panelden bir döşeme katmanı ekle!" + "addTileLayerFirst": "Öncelikle sol panelden bir döşeme katmanı ekle!", + "cacheLayer": "Bu katmanı önbelleğe al", + "cacheLayerWarning": "Önbelleğe almak döşeme katmanlarının renderlanmasını önemli bir seviyede hızlandırır. Bu seçeneği sadece döşeme katmanını oyunda dinamik olarak değiştirmen gerekiyorsa devre dışı bırakmalısın." }, "roomView": { "name": "İsim:", @@ -927,14 +985,17 @@ "disabled": "Devre dışı bırak:", "visible": "Görünürlük:", "tex": "Doku:", - "tint": "Renk Tonu:" + "tint": "Renk Tonu:", + "count": "Çatı sayısı:" }, "bindingTypes": { "string": "Metin değeri", "boolean": "Boolean değeri (doğru ya da yanlış)", "number": "Sayı" } - } + }, + "sendToBack": "Arkaya gönder", + "sendToFront": "Öne gönder" }, "styleView": { "active": "Aktif", @@ -967,7 +1028,9 @@ "strokeColor": "Hat rengi:", "strokeWeight": "Çizgi ağırlığı:", "testText": "Deneme yazısı 0123 +", - "fontSize": "Font boyutu:" + "fontSize": "Font boyutu:", + "fallbackFontFamily": "Fallback font ailesi:", + "notSupportedForBitmap": "Bu ayarların bitmap fontlarında desteklenmediğini not etmek gerekir." }, "textureView": { "center": "Eksen", @@ -1047,7 +1110,10 @@ }, "customCharsetHint": "İçermesini istediğin tüm harfleri yaz, büyük ve küçük harflerle.", "fontWeight": "Font ağırlığı:", - "typefaceName": "Yazı biçimi ismi:" + "typefaceName": "Yazı biçimi ismi:", + "addFont": "Font ekle...", + "pixelPerfect": "Kusursuz pikel hassasiyeti", + "pixelPerfectTooltip": "Bu pixelart fontlari için açılmalıdır. Bunun açılması 1 piksel genişliğindeki çizgilerin çizim hassasiyetini iyileştirecebileceği gibi her fontun harflerinin 4k'ya 4k olacak şekilde en büyük boyutlu dokusunun boyutunun azalmasını da sağlayabilir." }, "licensePanel": { "ctjsLicense": "Ct.js Lisansı (MIT)", @@ -1096,7 +1162,11 @@ "Text": "Yazı", "NineSlicePlane": "Panel", "Container": "Veri yapısı", - "Button": "Tuş" + "Button": "Tuş", + "BitmapText": "Bitmap yazısı", + "RepeatingTexture": "Tekrar eden doku", + "SpritedCounter": "Çatılı Tezgah", + "TextBox": "Yazı kutusu" }, "nineSliceTop": "Üst kesit, piksel cinsinden", "nineSliceRight": "Sağ kesit, piksel cinsinden", @@ -1104,7 +1174,22 @@ "nineSliceLeft": "Sol kesit, piksel cinsinden", "autoUpdateNineSlice": "Çarpışma şeklini otomatik olarak güncelle", "autoUpdateNineSliceHint": "Eğer bir panel boyutunu değiştirirse, çarpışma şeklini de otomatik olarak değiştirecektir. Buna genelde tam anlamıyla kozmetik ya da oluşumlarından sonra boyutunu asla değiştirmeyen ögeler için ihtiyaç duymazsın. u.reshapeNinePatch(this) aramasıyla hala çarpışma şeklini istediğin zaman güncelleyebilirsin.", - "panelHeading": "Doku kesme ayarları" + "panelHeading": "Doku kesme ayarları", + "useBitmapText": "Bitmap fontlarını kullan", + "errorBitmapNotConfigured": "Seçtiğin tarz bitmap fontu kullanan bir yazı tipine bağlı değil. Tarz sekmesine git, onu bir yazı tipine bağla ve bu yazı tipinin bir bitmap'i dışarı aktaracak şekilde ayarlandığından emin olup değişiklikleri uygula.", + "fieldType": "Alan türü:", + "fieldTypes": { + "text": "Yazı", + "number": "Sayı", + "email": "E-mail", + "password": "Şifre" + }, + "useCustomSelectionColor": "Seçili yazı için özel bir renk kullan", + "maxLength": "Maksimum uzunluk:", + "scrollSpeedX": "X cinsinden kaydırma hızı:", + "scrollSpeedY": "Y cinsinden kaydırma hızı:", + "isUi": "Arayüz zamanını kullan", + "defaultCount": "Varsayılan çatı sayısı:" }, "scriptables": { "addEvent": "Bir olay ekle", @@ -1121,7 +1206,8 @@ "misc": "Diğer", "animation": "Animasyon", "timers": "Zamanlayıcılar", - "app": "Uygulama" + "app": "Uygulama", + "input": "Giriş" }, "coreEvents": { "OnCreate": "Oluşturulduğuna", @@ -1146,7 +1232,11 @@ "OnAnimationComplete": "Animasyon tamamlandığında", "Timer": "Zamanlayıcı $1", "OnAppFocus": "Uygulama aktif", - "OnAppBlur": "Uygulama arkaplanda" + "OnAppBlur": "Uygulama arkaplanda", + "OnBehaviorAdded": "Davranış eklendi", + "OnBehaviorRemoved": "Davranış silindi", + "OnTextChange": "Yazı değişiminde", + "OnTextInput": "Yazı girişilişinde" }, "coreParameterizedNames": { "OnActionPress": "%%action%%a tıklandığında", @@ -1166,7 +1256,9 @@ }, "coreEventsLocals": { "OnActionDown_value": "Şu anki eylem'in değeri", - "OnActionPress_value": "Şu anki eylem'in değeri" + "OnActionPress_value": "Şu anki eylem'in değeri", + "OnTextChange_value": "Yeni yazı değeri", + "OnTextInput_value": "Yeni yazı değeri" }, "coreEventsDescriptions": { "OnCreate": "Kopyan oluşturulduğu zaman tetiklenir.", @@ -1182,12 +1274,18 @@ "OnAnimationComplete": "Döngüde olmayan bir animasyon bittiği zaman sadece 1 defa ateşlenir.", "Timer": "this.timer$1 = 3", "OnAppFocus": "Kullanıcı uygulamana geri gittiğinde tetiklenir", - "OnAppBlur": "Kullanıcı senin oyunundan başka bir şeye geçtiğinde tetiklenir — bu sekme değiştirmek, başka bir pencereye geçmek ya da oyunu küçültmek olabilir." + "OnAppBlur": "Kullanıcı senin oyunundan başka bir şeye geçtiğinde tetiklenir — bu sekme değiştirmek, başka bir pencereye geçmek ya da oyunu küçültmek olabilir.", + "OnBehaviorAdded": "Bu bu kopyaya herhangi bir davranış dinamik olarak eklendiğinde çağırılır. Bu olay statik davranışlarla çalışmaz; o yüzden onların yerine Oluşturma olaylarını kullan.", + "OnBehaviorRemoved": "Bu bu kopyadan herhangi bir davranış dinamik olarak silindiğinde çağırılır. Bu olay statik davranışlarla çalışmaz; o yüzden onların yerine Yok Etme olaylarını kullan.", + "OnTextChange": "Bir kullanıcı Enter tuşuna basarak ya da bu alanın dışına tıklayarak onu düzenlemeyi bitirdiğinde tetiklenir.", + "OnTextInput": "Bir kullanıcı bu alanın değerini değiştirdiği her zaman tetiklenir." }, "jumpToProblem": "Sorunun olduğu yere git", "staticEventWarning": "Bu olay bu davranışı statik yapıyor. Bu yüzden onu artık oyun içinden Davranış API'ı ile dinamik olarak ekleyemez ya da kaldıramazsın. Fakat onun dışında hiçbir sıkıntısı olmayacak.", "restrictedEventWarning": "Bu olay sadece temel sınıfı şu olan şablonlarla çalışacak: $1. Bu olay başka temel sınıflara uygulandığında çalışmayacak.", - "baseClassWarning": "Bu olay şu anki temel sınıfla çalışmazç" + "baseClassWarning": "Bu olay şu anki temel sınıfla çalışmazç", + "typedefs": "Ek tür tanımları:", + "typedefsHint": "Bu varlıkta fazladan özellikleri açıklayarak kesinlikle girildiklerinden emin olabilirsin.\nÖrnek:\n\nisim: string;\ncan: number;\ngüç: number;\nenvanter: any[];\n\nBu tamamen isteğe bağlı." }, "languageSelector": { "chooseLanguageHeader": "Yazılım dilini seç", @@ -1197,7 +1295,10 @@ "jsAndTs": "JavaScript", "jsTsDescription": "Web'in dili. Sözdizimi daha karmaşık ama kendi içinde editör-içi hata vurgulama ve kod önerileri var. Eğer önceden Java, C# ve JavaScript ile çalıştıysan JS'i seç.", "pickJsTs": "Javascript'i seçiyorum!", - "acceptAndSpecifyDirectory": "Kabul et ve proje klasörünü seç" + "acceptAndSpecifyDirectory": "Kabul et ve proje klasörünü seç", + "catnip": "Catnip", + "catnipDescription": "Ct.js için yapılmış görsel bir yazılımlama dili. Sürükle-bırak yaparak ve klavyeni kullanarak çeşitli blokları koyabilirsin. Eğer yazılımla ilgili hiçbir tecrüben yoksa bunu seçmek iyi bir karar olabilir.", + "pickCatnip": "Catnip'i seçiyorum!" }, "assetConfirm": { "confirmHeading": "İşlem seç", @@ -1225,7 +1326,11 @@ "eventConfiguration": "Olayların biri yanlış ayarlanmış ve içinde boş alanlar var. Varlığa git ve olayın parametrelerini düzenle.", "emptySound": "Seslerinin birinde ekli herhangi bir ses dosyası yok. Ona ya bir ses dosyası aktar ya da onu sil.", "emptyEmitter": "Parçacık sistemlerinden biri dağıtıcısında olmayan bir dokuya sahip. Ya ona bir doku belirlemen gerekecek ya da onu komple silmen gerekecek.", - "windowsFileLock": "Bu kilitli dosyada Windows'a özel bir sorun. Oyunu çalıştırmış her türlü tarayıcıyı kapatmış olduğundan emin olup bir daha dışarı aktarmayı dene. Eğer yardımcı olmazsa, ct.js'i yeniden başlat." + "windowsFileLock": "Bu kilitli dosyada Windows'a özel bir sorun. Oyunu çalıştırmış her türlü tarayıcıyı kapatmış olduğundan emin olup bir daha dışarı aktarmayı dene. Eğer yardımcı olmazsa, ct.js'i yeniden başlat.", + "eventMissing": "Varlığında kayıp bir modülü kullanan bir olay var. Lütfen gerekli olan tüm mödülleri kurup kurmadığını kontrol et ve eğer kısa bir süre önce catmods'u devre dışı bıraktıysan tekrardan aktif hale getirmeyi dene.", + "noTemplateTexture": "Şablonlarından bir tanesinin dokusu eksik. Ona bir doku eklemek zorundasın.", + "blockArgumentMissing": "Catnip kodundaki bir argüman ya düzgün ayarlanmadı ya da şu anda silinmiş olan bir varlığa bağlı. Lütfen mevzubahis varlığa git ve yukarıda bahsedilen bloğa bir değer ekle.", + "blockDeclarationMissing": "Catnip kodundaki bir blok eksik bir catmod'u kullanıyor. Eğer kısa süre önce catmod'ları devre dışı bıraktıysan onları geri açmayı deneyebilir ya da sorunlu blokları silebilirsin." }, "stacktrace": "Yığın ara", "jumpToProblem": "Soruna git", @@ -1243,7 +1348,9 @@ "scriptView": { "runAutomatically": "Oyun başlangıcında çalıştır", "language": "Dil:", - "convertToJavaScript": "JavaScript'e Dönüştür" + "convertToJavaScript": "JavaScript'e Dönüştür", + "confirmSwitchToCatnip": "Kodunu Catnip'e değiştirmek bı scriptteki kodların tamamını silecek. Devam etmek istediğinden emin misin?", + "confirmSwitchFromCatnip": "Kodunu Catnip'e dönüştürmek bu scriptteki kodların tamamını silecek. Eğer kodu muhafaza etmek istiyorsan scripti önce JavaScript'e dönüştürmelisin. Devam edip kodu temizlemek istediğinden emin misin" }, "soundView": { "variants": "Varyantlar", @@ -1263,5 +1370,434 @@ "falloff": "Düşüş:", "refDistance": "Düşüş başlangıcı:", "positionalAudioHint": "Bu sadece sounds.playAt metodunu etkiler. Düşüş bir sesin nasıl solacağını belirler, 0 hiç düşüş yok demekken çok yüksek miktarlar sesi anında sessizleştirebilir. Düşüş başlangıcı bir sesin solmaya başladıktan sonraki mesafesini belirler, örneğin 1 ekranın yarısıdır." + }, + "regionalLinks": { + "discord": "https://comigo.games/discord", + "telegram": "https://t.me/ctjsen" + }, + "catnip": { + "trashZoneHint": "Blokları hızlıca silmek için buraya bırak", + "properties": "Özellikler", + "propertiesHint": "Özellikler kopyada ya da odada saklanırlar ve sonradan başka kopyalar da dahil olarak erişilebilirler.", + "variables": "Değişkenler", + "variablesHint": "Değişkenler geçicidir ve sadece bu olayın çalıştığı süre boyunca varlıklarını sürdürürler. Hızlı hesaplamaların sonucunu depolamak için idealdirler.", + "globalVariables": "Küresel değişkenler", + "globalVariablesHint": "Küresel değişkenlere projenin her yerinde erişebilirsin. Çalıştırdığın farklı zamanlarda kaydedilmezler; bunun için \"Depolamaya kaydet\" ve \"Depolamadan yükle\" bloklarını kullanmalısın.", + "createNewProperty": "Yeni özellik", + "createNewVariable": "Yeni değişken", + "createNewGlobalVariable": "Yeni küresel değişken", + "newPropertyPrompt": "Yeni özelliğin ismini gir:", + "newVariablePrompt": "Yeni değişkenin ismini gir. İsimde alfabe dışı harf olmamalı, hiçbir özel karakter içermemeli ve bir harfle başlamalı.", + "newGlobalVariablePrompt": "Yeni küresel değişkenin ismini gir. İsimde alfabe dışı harf olmamalı, hiçbir özel karakter içermemeli ve bir harfle başlamalı.", + "invalidVarNameError": "Geçersiz karakter ismi. İsimde alfabe dışı harf olmamalı, hiçbir özel karakter içermemeli ve bir harfle başlamalı.", + "renamePropertyPrompt": "Bu özelliğin yeni ismini gir:", + "renameVariablePrompt": "Bu değişkenin yeni ismini gir:", + "renamingAcrossProject": "Diğer varlıklardaki değişken isimleri değiştiriliyor...", + "errorBlock": "Kütüphanede olmayan blok", + "errorBlockDeleteHint": "Silmek için sağ tıkla", + "asyncHint": "Bu blok eşzamanlı olmadan çalışır, yani daha sonra devreye girer ve scriptin kalanını engellemez. Tamamlanışında komutlar çalıştırmak için bunun içindeki komut bölgelerini kullanabilirsin fakat bazı şeylerin bu blok çalışırken değişebileceğini söylemekte fayda var: mesela -oynanış mantığına bağlı olarak- bu bloğu çalıştıran kopya silinebilir ve böylece bu blok bittiğinde tamamen kullanışsız hale gelebilir.", + "optionsAdvanced": "Gelişmiş", + "addCustomOption": "Özel bir özellik ekle", + "changeBlockTo": "\"$1\"'e değiştir", + "goToActions": "Eylem Ayarlarını aç", + "copyDocHtml": "Dökümentasyon için HTML olarak kopyala", + "copySelection": "Seçili blokları kopyala", + "duplicateBlock": "Bu bloğu çoğalt", + "requiredField": "Bu alanın doldurulması zorunludur ve şu an eksiktir.", + "unnamedGroup": "İsimlendirilmemiş grup", + "placeholders": { + "putBlocksHere": "Bloklarını buraya koy", + "doNothing": "Hiçbir şey yapma" + }, + "coreLibs": { + "appearance": "Görünüm", + "arrays": "Diziler", + "backgrounds": "Arkaplanlar", + "behaviors": "Davranışlar", + "camera": "Kamera", + "console": "Konsol", + "emitter tandems": "Dağıtıcılar", + "logic": "Mantık", + "math": "Matematik", + "misc": "Çeşitli", + "movement": "Hareket", + "objects": "Objeler", + "rooms": "Odalar", + "settings": "Ayarlar", + "sounds": "Sesler", + "strings": "Stringler", + "templates": "Şablonlar", + "utilities": "Yararlılıklar", + "actions": "Eylemler", + "timers": "Zamanlayıcılar" + }, + "blockNames": { + "kill copy": "Bu kopyayı yok et", + "move copy": "Bu kopyayı hareket ettir", + "set speed": "Hızı şuna ayarla", + "set gravity": "Yer çekimini şuna ayarla", + "set gravityDir": "Yer çekimi yönünü şuna ayarla", + "set hspeed": "Yatay hızı şuna ayarla", + "set vspeed": "Dikey hızı şuna ayarla", + "set direction": "Yönü şuna ayarla", + "get speed": "hız", + "get gravity": "yer çekimi", + "get gravityDir": "yer çekimi yönü", + "get hspeed": "yatay hız", + "get vspeed": "dikey hız", + "get direction": "yön", + "y of copy": "bir kopyanın y'si", + "x of copy": "bir kopyanın x'si", + "get width": "genişlik", + "get height": "yükseklik", + "set width": "Genişliği şuna ayarla", + "set height": "Yüksekliği şuna ayarla", + "set property variable": "Özellik/değişken ayarla", + "increment": "Artma", + "decrement": "Azalma", + "increase": "Arttır", + "decrease": "Azalt", + "this write": "Yaz", + "current room write": "Şu anki odaya yaz", + "write property to object": "Objeye yaz", + "this read": "oku", + "room read": "şu anki odanın özelliği", + "object read": "objenin özelliği", + "object delete": "Bir objedeki özelliği sil", + "new array": "yeni dizi", + "new object": "Yeni bir obje oluştur", + "new empty object": "yeni boş obje", + "convert to string": "string'e", + "const string": "String (sabit bir değer)", + "convert to number": "sayıya dönüştür", + "convert to boolean": "boolean'a", + "note": "Not", + "plainJs": "JavaScript Çalıştır", + "color": "renk", + "variable": "Değişken", + "property": "Özellik", + "behavior property": "Davranışın özelliği", + "content type entries": "İçerik türü girişler", + "if else branch": "Eğer-değilse dalı", + "if branch": "Eğer dalı", + "while loop cycle": "Döngüyken", + "repeat": "N kere tekrar et", + "for each": "Dizinin her elemanı için", + "break loop": "Bu döngüyü durdur", + "NOT logic operator": "NOT mantık operatörü", + "AND logic operator": "VE mantık operatörü", + "OR logic operator": "VEYA mantık operatörü", + "AND AND logic operator": "çifte VE mantık operatörü", + "OR OR logic operator": "çifte VEYA mantık operatörü", + "is": "Eşittir (şuna eşittir)", + "is not": "Değildir (şuna eşit değildir)", + "set texture": "Dokuyu ayarla", + "set scale": "Ölçeği ayarla", + "set scale xy": "Ölçeği ayarla", + "set angle": "Doku dönüşünü ayarla", + "set skew": "Çarpıklığı ayarla", + "skew x": "x'e göre çarpıt", + "skew y": "y'ye göre çarpıt", + "set alpha": "Şeffaflığı ayarla", + "scale x": "x'e göre ölçekle", + "scale y": "y'ye göre ölçekle", + "get angle": "doku dönüşü", + "get alpha": "şeffaflık", + "set tint": "Tonlamayı ayarla", + "get tint": "tonlama", + "play animation": "Animasyonu oynat", + "stop animation": "Animasyonu durdur", + "goto frame play": "Bir kareye git ve animasyonu oynat", + "goto frame stop": "Bir kareye git ve animasyonu durdur", + "goto frame": "Bir kareye git", + "get animation speed": "animasyon hızı", + "set animation speed": "Animasyon hızını ayarla", + "this": "bu", + "concatenate strings": "Stringleri bağla", + "concatenate strings triple": "Stringleri bağla (üçlü)", + "templates Templates copy into room": "Bir şablonu odaya kopyala", + "templates Templates copy": "Bir şablonu kopyala", + "templates Templates each": "Her kopya için", + "templates Templates with copy": "", + "templates Templates with template": "", + "templates templates exists": "", + "rooms Rooms add bg": "", + "rooms Rooms clear": "", + "rooms Rooms remove": "", + "rooms Rooms switch": "", + "rooms Rooms restart": "", + "rooms Rooms append": "", + "rooms Rooms prepend": "", + "rooms Rooms merge": "", + "rooms rooms current": "", + "rooms rooms list": "", + "rooms rooms starting": "", + "behaviors Behaviors add": "", + "behaviors Behaviors remove": "", + "behaviors behaviors has": "", + "sounds Sounds play": "", + "sounds Sounds play at": "", + "sounds Sounds stop": "", + "sounds Sounds pause": "", + "sounds Sounds resume": "", + "sounds Sounds global volume": "", + "sounds Sounds fade": "", + "sounds Sounds add filter": "", + "sounds Sounds add distortion": "", + "sounds Sounds add equalizer": "", + "sounds Sounds add mono filter": "", + "sounds Sounds add reverb": "", + "sounds Sounds add stereo filter": "", + "sounds Sounds add panner filter": "", + "sounds Sounds add telephone": "", + "sounds Sounds remove filter": "", + "sounds Sounds speed all": "", + "sounds sounds load": "", + "sounds sounds exists": "", + "sounds sounds playing": "", + "sounds sounds toggle mute all": "", + "sounds sounds toggle pause all": "", + "styles styles get": "", + "backgrounds Backgrounds add": "", + "backgrounds backgrounds list": "", + "emitter tandems Emitters fire": "", + "emitter tandems Emitters append": "", + "emitter tandems Emitters follow": "", + "emitter tandems Emitters stop": "", + "emitter tandems Emitters pause": "", + "emitter tandems Emitters resume": "", + "emitter tandems Emitters clear": "", + "utilities U reshape nine patch": "", + "utilities u time": "", + "utilities u time ui": "", + "utilities u get environment": "", + "utilities u get os": "", + "utilities u ldx": "", + "utilities u ldy": "", + "utilities u pdn": "", + "utilities u pdc": "", + "utilities u deg to rad": "", + "utilities u rad to deg": "", + "utilities u rotate": "", + "utilities u rotate rad": "", + "utilities u delta dir": "", + "utilities u clamp": "", + "utilities u lerp": "", + "utilities u unlerp": "", + "utilities u map": "", + "utilities u get rect shape": "", + "utilities u prect": "", + "utilities u pcircle": "", + "utilities u ui to css coord": "", + "utilities u game to css coord": "", + "utilities u ui to css scalar": "", + "utilities u game to css scalar": "", + "utilities u game to ui coord": "", + "utilities u ui to game coord": "", + "utilities U wait": "", + "utilities U wait ui": "", + "utilities u numbered string": "", + "utilities u get string number": "", + "settings settings high density": "", + "settings settings target fps": "", + "settings settings view mode": "", + "settings settings fullscreen": "", + "settings settings pixelart": "", + "settings settings prevent default": "", + "set x": "", + "set y": "", + "get x": "", + "get y": "", + "follow this": "", + "follow": "", + "set zoom": "", + "get zoom": "", + "set targetX": "", + "set targetY": "", + "set shiftX": "", + "set shiftY": "", + "set drift": "", + "set rotation": "", + "set followX": "", + "set followY": "", + "set borderX": "", + "set borderY": "", + "set shake": "", + "set shakeDecay": "", + "set shakeFrequency": "", + "set shakeX": "", + "set shakeY": "", + "set shakeMax": "", + "set minX": "", + "set maxX": "", + "set minY": "", + "set maxY": "", + "get targetX": "", + "get targetY": "", + "get computedX": "", + "get computedY": "", + "get shiftX": "", + "get shiftY": "", + "get drift": "", + "get left": "", + "get right": "", + "get top": "", + "get bottom": "", + "get rotation": "", + "get followX": "", + "get followY": "", + "get borderX": "", + "get borderY": "", + "get shake": "", + "get shakeDecay": "", + "get shakeFrequency": "", + "get shakeX": "", + "get shakeY": "", + "get shakeMax": "", + "get minX": "", + "get maxX": "", + "get minY": "", + "get maxY": "", + "console log": "", + "console warn": "", + "console error": "", + "set depth": "", + "get depth": "", + "script options": "", + "run script": "", + "hasSubstring": "", + "substringPosition": "", + "stringLength": "", + "replace substring": "", + "replace all substrings": "", + "trim whitespace": "", + "regex passes": "", + "replace by regex": "", + "replace all by regex": "", + "split by a substring": "", + "split": "", + "slice a string": "", + "to uppercase": "", + "to lowercase": "", + "array unshift": "", + "array push": "", + "add element at position": "", + "array pop": "", + "array shift": "", + "remove element from array": "", + "remove at position": "", + "filter array": "", + "map array": "", + "set text": "", + "set disabled": "", + "get text": "", + "get disabled": "", + "define function": "", + "return": "", + "execute function": "", + "get function option": "", + "set timer 1": "", + "set timer 2": "", + "set timer 3": "", + "set timer 4": "", + "set timer 5": "", + "set timer 6": "", + "get timer 1": "", + "get timer 2": "", + "get timer 3": "", + "get timer 4": "", + "get timer 5": "", + "get timer 6": "", + "set game speed": "", + "get game speed": "", + "deserialize object": "", + "serialize object": "", + "save to storage": "", + "delete from storage": "", + "load from storage": "", + "is key in storage": "", + "owning room": "" + }, + "blockDisplayNames": { + "write": "Yaz", + "if else branch": "Eğer", + "while loop cycle": "İken", + "repeat": "Tekrarla", + "for each": "Her için", + "NOT logic operator": "değil", + "set": "Ayarla", + "x of": "x'i", + "y of": "y'si", + "options": "seçenekler", + "lengthOf": "uzunluğu", + "split": "böl", + "join": "katıl", + "add element": "Bir element ekle", + "remove element": "Elementi kaldır", + "set timer 1 to": "1. zamanlayıcıyı şuna ayarla", + "set timer 2 to": "2. zamanlayıcıyı şuna ayarla", + "set timer 3 to": "3. zamanlayıcıyı şuna ayarla", + "set timer 4 to": "4. zamanlayıcıyı şuna ayarla", + "set timer 5 to": "5. zamanlayıcıyı şuna ayarla", + "set timer 6 to": "6. zamanlayıcıyı şuna ayarla", + "timer": "zamanlayıcı", + "read": "oku", + "set game speed to": "Oyun hızını şuna ayarla", + "game speed": "oyun hızı" + }, + "blockLabels": { + "value": "değer", + "property": "özellik", + "changeBy": "tarafından", + "is not": "-dir değilse", + "is": "-dir", + "else": "Değilse", + "timesCount": "zamanlar", + "toWrite": "ona", + "fromRead": "ondan", + "atPosition": "onda", + "inInside": "içinde", + "contains": "içerir", + "forDuring": "için", + "replace": "değiştir", + "replaceAll": "hepsini değiştir", + "replaceByRegex": "regex'ten değiştir", + "replaceAllByRegex": "hepsini regex'ten değiştir", + "fromDestination": "ondan", + "fromSource": "ondan", + "toDestination": "ona", + "store index in": "dizini şurada depola", + "store in": "şurada depola", + "store result in": "sonucu şurada depola", + "of array": "dizinin", + "of current room": "şu anki odanın", + "to current room": "şu anki odaya", + "AND": "ve", + "OR": "veya", + "and": "ve", + "then": "Sonra", + "catch": "Hatada", + "and play animation": "ve animasyonu oynat", + "and stop animation": "ve animasyonu durdur", + "scale": "ölçek", + "position": "konum", + "at position": "konumda", + "with results in": "sonuçlar depolanmışken", + "store new array in": "yeni diziyi şurada depola", + "and elements": "ve elementler", + "secondsUnits": "saniye(ler)" + }, + "blockOptions": { + "soundVolume": "Ses", + "loop": "Döngü", + "isRoomUi": "Bu oda bir arayüz katmanı mı?", + "speed": "Hız", + "start at": "Başla", + "soundSingleInstance": "Diğer ses örneklerini durdur" + }, + "blockDocumentation": { + "serialize object": "", + "constant string": "Bu bloğu bir stringin zorla oluşturulması için kullanabilirsin: mesela bir stringe bir sayı yazmak ya da \\n'i bir satır sonuna dönüştürmek istediğinde bunu joker slotuna koyarak yapabilirsin." + } } } From 659bf4192152aa82460c9aaf13e291b0461615ff Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:16:24 +1200 Subject: [PATCH 37/44] :zap: Tweak styles of menus a bit so they don't change the height of a menu item when hovered --- src/styl/buildingBlocks.styl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/styl/buildingBlocks.styl b/src/styl/buildingBlocks.styl index 3c708dfba..0544272b5 100644 --- a/src/styl/buildingBlocks.styl +++ b/src/styl/buildingBlocks.styl @@ -100,13 +100,15 @@ {transshort} border-bottom-color borderBright color acttext - padding 0.2em 0.5em 0.2em 1.1em + padding-right 0.5em + padding-left 1.1em &.checkbox padding 0.2em 0.5em 0.2em 1.5em input[type="checkbox"], input[type="radio"] top 0.65rem &:hover, &.hover - padding 0.2em 0.5em 0.2em 1.5em + padding-right 0.5em + padding-left 1.5em & > svg:first-child, & > .aSpacer:first-child, & > img.icon:first-child color act margin-right 0.5rem From 4fe34c183071057dd4707dd458861b45b4f94bf8 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:27:02 +1200 Subject: [PATCH 38/44] :zap: Navigate through catnip in-place block search with arrow keys --- src/riotTags/catnip/catnip-insert-mark.tag | 27 +++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/riotTags/catnip/catnip-insert-mark.tag b/src/riotTags/catnip/catnip-insert-mark.tag index 3758d8795..67eb92c81 100644 --- a/src/riotTags/catnip/catnip-insert-mark.tag +++ b/src/riotTags/catnip/catnip-insert-mark.tag @@ -19,12 +19,12 @@ catnip-insert-mark(onclick="{toggleMenu}" class="{dragover: shouldDragover(), me type="text" value="{searchVal}" oninput="{search}" onclick="{selectSearch}" - onkeyup="{tryHotkeys}" + onkeydown="{tryHotkeys}" ) svg.feather use(href="#search") ul.aMenu.aPanel(role="menu" class="{up: menuUp}" if="{opened && searchVal.trim() && searchResults.length}") - li(role="menuitem" each="{block in searchResults}" onpointerdown="{insertBlock}" tabindex="0" onkeyup="{menuKeyDown}") + li(role="menuitem" each="{block in searchResults}" onpointerdown="{insertBlock}" tabindex="0" onkeydown="{menuKeyDown}" ref="results") code.toright.inline.small.dim {block.lib} svg.feather use(href="#{block.icon}") @@ -95,6 +95,12 @@ catnip-insert-mark(onclick="{toggleMenu}" class="{dragover: shouldDragover(), me } }); } + } else if (e.key === 'ArrowDown') { + const refs = this.refs.results; + const items = Array.isArray(refs) ? refs : [refs]; + if (items.length) { + items[0].focus(); + } } }; @@ -116,7 +122,22 @@ catnip-insert-mark(onclick="{toggleMenu}" class="{dragover: shouldDragover(), me this.menuKeyDown = e => { if (e.code === 'Enter' || e.code === 'Space') { this.insertBlock(e); - } else if (e.code !== 'Tab') { + } else if (e.code === 'ArrowUp') { + const current = this.root.querySelector(':focus'); + if (current.previousElementSibling) { + current.previousElementSibling.focus(); + } else { + this.refs.search.select(); + } + e.preventDefault(); + } else if (e.code === 'ArrowDown') { + const current = this.root.querySelector(':focus'); + if (current.nextElementSibling) { + current.nextElementSibling.focus(); + } + e.preventDefault(); + } else if (e.code !== 'Tab' && e.code !== 'ShiftLeft' && e.code !== 'ShiftRight') { + // Shift & Tab are used to navigate the keyboard focus this.opened = false; this.parent.update(); } else { From bf7b4747ecd5987744519025b0c4648ce311ab8d Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:27:40 +1200 Subject: [PATCH 39/44] =?UTF-8?q?:sparkles:=20Global=20asset=20search=20ac?= =?UTF-8?q?cessible=20with=20the=20=F0=9F=94=8D=20icon=20in=20the=20top-ri?= =?UTF-8?q?ght=20and=20the=20Ctrl+P=20hotkey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/data/i18n/English.json | 6 ++ src/js/utils/codeEditorHelpers.js | 8 ++ src/node_requires/resources/index.ts | 14 +++ src/riotTags/app-view.tag | 20 +++- src/riotTags/search-and-recents.tag | 133 ++++++++++++++++++++++++++ src/styl/tags/search-and-recents.styl | 15 +++ 6 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/riotTags/search-and-recents.tag create mode 100644 src/styl/tags/search-and-recents.styl diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index 5711abe78..a99cc39e6 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -746,6 +746,12 @@ "icon": "Icon:", "color": "Color:" }, + "globalSearch": { + "buttonTitle": "Search assets", + "nothingFound": "Nothing found.", + "searchHint": "Search for any asset by its name. Recent assets will also go here.", + "recent": "recently opened" + }, "intro": { "loading": "Please wait: kittens are gathering speed of light!", "newUserHeader": "Welcome to ct.js!", diff --git a/src/js/utils/codeEditorHelpers.js b/src/js/utils/codeEditorHelpers.js index ee073264d..e128b0799 100644 --- a/src/js/utils/codeEditorHelpers.js +++ b/src/js/utils/codeEditorHelpers.js @@ -168,6 +168,14 @@ }); document.body.dispatchEvent(event); }); + editor.addCommand(monaco.KeyCode.KeyP | monaco.KeyMod.CtrlCmd, () => { + const event = new KeyboardEvent('keydown', { + key: 'p', + code: 'p', + ctrlKey: true + }); + document.body.dispatchEvent(event); + }); }; const isRangeSelection = function (s) { diff --git a/src/node_requires/resources/index.ts b/src/node_requires/resources/index.ts index ff92725e9..00a040ff5 100644 --- a/src/node_requires/resources/index.ts +++ b/src/node_requires/resources/index.ts @@ -110,6 +110,18 @@ export const folderMap: Map = new Map(); * This is used to delete assets from them. */ export const collectionMap: Map = new Map(); + +// Fuzzy search +import {default as Fuse} from 'fuse.js'; +const fuse = new Fuse([], { + keys: ['name'], + threshold: 0.4, + findAllMatches: true +}); +export const searchAssets = (query: string): IAsset[] => fuse.search(query, { + limit: 30 +}).map(({item}) => item); + /** * An operation that fills the asset map, which is a mandatory operation for project loading. */ @@ -130,6 +142,7 @@ export const buildAssetMap = (project: IProject): void => { uidMap.set(entry.uid, entry); folderMap.set(entry, current); collectionMap.set(entry, entries); + fuse.add(entry); } else { recursiveFolderWalker(entry, entry.entries); } @@ -543,6 +556,7 @@ export const editorMap: Record = { enum: 'enum-editor' }; + export { textures, emitterTandems, diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag index e6d44651c..c257a5b7b 100644 --- a/src/riotTags/app-view.tag +++ b/src/riotTags/app-view.tag @@ -48,6 +48,7 @@ app-view.flexcol use(xlink:href="#x") svg.feather.anActionableIcon(if="{!tabsDirty[ind]}" onclick="{closeAsset}") use(xlink:href="#x") + search-and-recents.nogrow div.flexitem.relative(if="{window.currentProject}") main-menu(show="{tab === 'menu'}" ref="mainMenu") debugger-screen-multiwindow(if="{tab === 'debug' && !splitDebugger}" params="{debugParams}" data-hotkey-scope="play" ref="debugger") @@ -122,6 +123,7 @@ app-view.flexcol span {voc.applyAndRun} script. const fs = require('fs-extra'); + const {saveProject, getProjectCodename} = require('src/node_requires/resources/projects'); this.namespace = 'appView'; this.mixin(require('src/node_requires/riotMixins/voc').default); @@ -189,6 +191,10 @@ app-view.flexcol const resources = require('src/node_requires/resources'); this.editorMap = resources.editorMap; this.iconMap = resources.resourceToIconMap; + + this.recentAssets = localStorage[`lastOpened_${getProjectCodename()}`] ? + JSON.parse(localStorage[`lastOpened_${getProjectCodename()}`]) : + []; this.openAsset = (asset, noOpen) => () => { // Check whether the asset is not yet opened if (!this.openedAssets.includes(asset)) { @@ -213,9 +219,20 @@ app-view.flexcol }, 100); } else if (noOpen) { // eslint-disable-next-line no-console - console.warn('[app-view] An already opened asset was called with noOpen. This is probably a bug as you either do open assets or create them elsewhere without opening.'); + console.warn('[app-view] An already opened asset was called with noOpen. ' + + 'This is probably a bug as you either do open assets or create them elsewhere without opening.'); } if (!noOpen) { + // Remember recently opened assets for the global asset search + if (this.recentAssets.indexOf(asset.uid) !== -1) { + this.recentAssets.splice(this.recentAssets.indexOf(asset.uid), 1); + } + this.recentAssets.unshift(asset.uid); + if (this.recentAssets.length > 10) { + this.recentAssets.length = 10; + } + localStorage[`lastOpened_${getProjectCodename()}`] = JSON.stringify(this.recentAssets); + this.changeTab(asset)(); } }; @@ -325,7 +342,6 @@ app-view.flexcol }; // Remember assets opened before closing the editor and load them on project load. - const {saveProject, getProjectCodename} = require('src/node_requires/resources/projects'); const saveOpenedAssets = () => { const openedIds = this.openedAssets.map(a => a.uid); localStorage[`lastOpened_${getProjectCodename()}`] = JSON.stringify(openedIds); diff --git a/src/riotTags/search-and-recents.tag b/src/riotTags/search-and-recents.tag new file mode 100644 index 000000000..893fc76cc --- /dev/null +++ b/src/riotTags/search-and-recents.tag @@ -0,0 +1,133 @@ +search-and-recents.aNav(class="{opts.class}") + li(onclick="{toggleMenu}" data-hotkey="Control+p" role="button" title="{voc.buttonTitle} Ctrl+P") + svg.feather + use(href="#search") + .aDimmer(if="{opened}" onclick="{closeOnOutsideClick}" ref="dimmer") + .aSearchWrap + input.wide( + ref="search" + type="text" value="{searchVal}" + oninput="{search}" + onkeydown="{tryHotkeys}" + placeholder="{voc.searchHint}" + ) + svg.feather + use(href="#search") + ul.aMenu.aPanel(role="menu" if="{opened}" ref="menu") + // Search items + li( role="menuitem" tabindex="0" ref="searchItems" + if="{searchVal.trim() && searchResults.length}" each="{asset in searchResults}" + onpointerdown="{openAsset}" onkeydown="{menuKeyDown}" + ) + svg.feather + use(href="#{iconMap[asset.type]}") + span {asset.name} + | + | + span.small.dim {vocGlob.assetTypes[asset.type][0]} + li(disabled="disabled" if="{searchVal.trim() && !searchResults.length}").dim {voc.nothingFound} + + // Recent items + li( role="menuitem" tabindex="0" ref="lastItems" + onpointerdown="{openAssetById}" onkeydown="{menuKeyDown}" + if="{!searchVal.trim()}}" each="{uid in recentAssets}" + ) + svg.feather + use(href="#{iconMap[getById(uid).type]}") + span {getById(uid).name} + | + | + span.small.dim {vocGlob.assetTypes[getById(uid).type][0]} + .toright.small.dim {voc.recent} + script. + const {searchAssets, resourceToIconMap, getById} = require('src/node_requires/resources'); + const {getProjectCodename} = require('src/node_requires/resources/projects'); + + this.namespace = 'globalSearch'; + this.mixin(require('src/node_requires/riotMixins/voc').default); + this.getById = id => getById(null, id); + + this.opened = false; + this.iconMap = resourceToIconMap; + + this.toggleMenu = e => { + e.preventDefault(); + this.opened = !this.opened; + this.searchVal = ''; + if (this.opened) { + setTimeout(() => { + this.refs.search.focus(); + }, 0); + this.recentAssets = localStorage[`lastOpened_${getProjectCodename()}`] ? + JSON.parse(localStorage[`lastOpened_${getProjectCodename()}`]) : + []; + } + }; + + this.search = e => { + this.searchVal = e.target.value; + if (this.searchVal.trim()) { + this.searchResults = searchAssets(this.searchVal.trim()); + } + }; + + this.closeOnOutsideClick = e => { + if (e.target === this.refs.dimmer) { + this.opened = false; + } + }; + + this.tryHotkeys = e => { + if (e.key === 'Escape') { + this.opened = false; + } else if (e.key === 'Enter') { + if (this.searchVal.trim() && this.searchResults.length) { + this.opened = false; + window.orders.trigger('openAsset', this.searchResults[0].uid); + } else if (this.recentAssets.length) { + this.opened = false; + window.orders.trigger('openAsset', this.recentAssets[0].uid); + } + } else if (e.key === 'ArrowDown') { + const refs = this.refs.searchItems ?? this.refs.lastItems; + const items = Array.isArray(refs) ? refs : [refs]; + if (items.length) { + items[0].focus(); + } + } + }; + this.openAsset = e => { + const {asset, uid} = e.item; + this.opened = false; + window.orders.trigger('openAsset', asset?.uid ?? uid); + }; + + // Events happening while navigating the menu items with keyboard + this.menuKeyDown = e => { + if (e.code === 'Enter' || e.code === 'Space') { + const {asset, uid} = e.item; + this.opened = false; + window.orders.trigger('openAsset', asset?.uid ?? uid); + } else if (e.code === 'ArrowRight') { + const {asset, uid} = e.item; + window.orders.trigger('openAsset', asset?.uid ?? uid); + } else if (e.code === 'ArrowUp') { + const current = this.root.querySelector(':focus'); + if (current.previousElementSibling) { + current.previousElementSibling.focus(); + } else { + this.refs.search.select(); + } + e.preventDefault(); + } else if (e.code === 'ArrowDown') { + const current = this.root.querySelector(':focus'); + if (current.nextElementSibling) { + current.nextElementSibling.focus(); + } + e.preventDefault(); + } else if (e.code !== 'Tab' && e.code !== 'ShiftLeft' && e.code !== 'ShiftRight') { + // Shift & Tab are used to navigate the keyboard focus + this.opened = false; + this.parent.update(); + } + }; diff --git a/src/styl/tags/search-and-recents.styl b/src/styl/tags/search-and-recents.styl new file mode 100644 index 000000000..e7dda3c9c --- /dev/null +++ b/src/styl/tags/search-and-recents.styl @@ -0,0 +1,15 @@ +search-and-recents.aNav + border-left 0 + border-top 0 + & > li + padding-left 0.65rem + padding-right @padding-left + .aDimmer + padding 3.5rem 25% + box-sizing border-box + justify-content start + align-items stretch + .aPanel + max-height calc(min(60%, 25rem)) + li + text-align left From e76a43df11b49b34b14ba4018cd05ea9e2f10ee0 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:39:30 +1200 Subject: [PATCH 40/44] :hankey: Fix various bugs witht the search menu --- src/riotTags/app-view.tag | 15 ++++++++------- src/riotTags/catnip/catnip-insert-mark.tag | 1 + src/riotTags/search-and-recents.tag | 10 ++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag index c257a5b7b..93cde2461 100644 --- a/src/riotTags/app-view.tag +++ b/src/riotTags/app-view.tag @@ -124,6 +124,10 @@ app-view.flexcol script. const fs = require('fs-extra'); const {saveProject, getProjectCodename} = require('src/node_requires/resources/projects'); + const resources = require('src/node_requires/resources'); + + this.editorMap = resources.editorMap; + this.iconMap = resources.resourceToIconMap; this.namespace = 'appView'; this.mixin(require('src/node_requires/riotMixins/voc').default); @@ -188,12 +192,8 @@ app-view.flexcol window.signals.off('assetRemoved', checkDeletedTabs); }); - const resources = require('src/node_requires/resources'); - this.editorMap = resources.editorMap; - this.iconMap = resources.resourceToIconMap; - - this.recentAssets = localStorage[`lastOpened_${getProjectCodename()}`] ? - JSON.parse(localStorage[`lastOpened_${getProjectCodename()}`]) : + this.recentAssets = localStorage[`recentlyOpened_${getProjectCodename()}`] ? + JSON.parse(localStorage[`recentlyOpened_${getProjectCodename()}`]) : []; this.openAsset = (asset, noOpen) => () => { // Check whether the asset is not yet opened @@ -228,10 +228,11 @@ app-view.flexcol this.recentAssets.splice(this.recentAssets.indexOf(asset.uid), 1); } this.recentAssets.unshift(asset.uid); + this.recentAssets = this.recentAssets.filter(a => resources.exists(null, a)); if (this.recentAssets.length > 10) { this.recentAssets.length = 10; } - localStorage[`lastOpened_${getProjectCodename()}`] = JSON.stringify(this.recentAssets); + localStorage[`recentlyOpened_${getProjectCodename()}`] = JSON.stringify(this.recentAssets); this.changeTab(asset)(); } diff --git a/src/riotTags/catnip/catnip-insert-mark.tag b/src/riotTags/catnip/catnip-insert-mark.tag index 67eb92c81..449311460 100644 --- a/src/riotTags/catnip/catnip-insert-mark.tag +++ b/src/riotTags/catnip/catnip-insert-mark.tag @@ -100,6 +100,7 @@ catnip-insert-mark(onclick="{toggleMenu}" class="{dragover: shouldDragover(), me const items = Array.isArray(refs) ? refs : [refs]; if (items.length) { items[0].focus(); + e.preventDefault(); } } }; diff --git a/src/riotTags/search-and-recents.tag b/src/riotTags/search-and-recents.tag index 893fc76cc..2e2ff4260 100644 --- a/src/riotTags/search-and-recents.tag +++ b/src/riotTags/search-and-recents.tag @@ -29,7 +29,7 @@ search-and-recents.aNav(class="{opts.class}") // Recent items li( role="menuitem" tabindex="0" ref="lastItems" - onpointerdown="{openAssetById}" onkeydown="{menuKeyDown}" + onpointerdown="{openAsset}" onkeydown="{menuKeyDown}" if="{!searchVal.trim()}}" each="{uid in recentAssets}" ) svg.feather @@ -40,7 +40,7 @@ search-and-recents.aNav(class="{opts.class}") span.small.dim {vocGlob.assetTypes[getById(uid).type][0]} .toright.small.dim {voc.recent} script. - const {searchAssets, resourceToIconMap, getById} = require('src/node_requires/resources'); + const {searchAssets, resourceToIconMap, getById, exists} = require('src/node_requires/resources'); const {getProjectCodename} = require('src/node_requires/resources/projects'); this.namespace = 'globalSearch'; @@ -58,9 +58,10 @@ search-and-recents.aNav(class="{opts.class}") setTimeout(() => { this.refs.search.focus(); }, 0); - this.recentAssets = localStorage[`lastOpened_${getProjectCodename()}`] ? - JSON.parse(localStorage[`lastOpened_${getProjectCodename()}`]) : + this.recentAssets = localStorage[`recentlyOpened_${getProjectCodename()}`] ? + JSON.parse(localStorage[`recentlyOpened_${getProjectCodename()}`]) : []; + this.recentAssets = this.recentAssets.filter(a => exists(null, a)); } }; @@ -93,6 +94,7 @@ search-and-recents.aNav(class="{opts.class}") const items = Array.isArray(refs) ? refs : [refs]; if (items.length) { items[0].focus(); + e.preventDefault(); } } }; From 5a73b9c42fb554e78e31c442aa14761b98cc1f75 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:41:38 +1200 Subject: [PATCH 41/44] :bookmark: Bump version number --- app/package-lock.json | 4 ++-- app/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index b1cb67bc4..a2f6340b3 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctjs", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctjs", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@neutralinojs/neu": "^11.0.1", diff --git a/app/package.json b/app/package.json index c35a7fb27..f8f23f130 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "main": "index.html", "name": "ctjs", "description": "ct.js — a free 2D game engine", - "version": "5.0.1", + "version": "5.1.0", "homepage": "https://ctjs.rocks/", "author": { "name": "Cosmo Myzrail Gorynych", diff --git a/package-lock.json b/package-lock.json index aade0b09e..4d43e55b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctjsbuildenvironment", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ctjsbuildenvironment", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "@capacitor/cli": "^5.5.0", diff --git a/package.json b/package.json index da55b5b75..05830ddf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctjsbuildenvironment", - "version": "5.0.1", + "version": "5.1.0", "description": "", "directories": { "doc": "docs" From 9c2d143b6a2ce19b15d6ff2074626db06d37039b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:42:21 +1200 Subject: [PATCH 42/44] :bento: Pull the v5.1 docs --- docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs b/docs index 296e50170..cdf661cea 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 296e50170da1a5ebe5ae053fc71192938653064e +Subproject commit cdf661cea6d12d43088523521f1824791a83b2c1 From b7d98621deae4a08b1c296630e9481b8aebfc3ba Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:48:59 +1200 Subject: [PATCH 43/44] :pencil: Update the changelog --- app/Changelog.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/app/Changelog.md b/app/Changelog.md index 051200582..26ea3bb44 100644 --- a/app/Changelog.md +++ b/app/Changelog.md @@ -1,3 +1,65 @@ +## v5.1.0 + +*Sat Jul 20 2024* + +### ✨ New Features + +* Enumeration asset type to create lists of predefined values for content types, behaviors, and to be used directly in code +* Global asset search accessible with the 🔍 icon in the top-right and the Ctrl+P hotkey +* Map data type in content types and behaviors' fields +* New `random.enumValue` method +* Paste textures with Ctrl+V while on the Assets tab +* Pixel-perfect mode for Scrolling Texture base class +* Place filled rectangles of copies or tiles with Shift+Ctrl modifier in a room editor +* Room editor: show a counter when placing copies or tiles in a straight line (with a Shift key) + +### ⚡️ General Improvements + +* Allow closing most success/error/warning messages in the bottom-right corner with a click +* Catnip: Add xprev, yprev blocks to the Movement category +* Disable Vulkan support by default due to frequent issues with it on Linux +* Ignore actions on not-yet loaded sounds; improve migration from v3 to v5 (#532 by @godmar) + - sound actions on sounds that haven't been loaded are now ignored + - sound.playing returns false for sounds not yet loaded instead of crashing + - strip ct from ct.tween during migration + - delete deprecated mouse catmod on 4.0.1 migration to prevent crash +* Internal: Improve how ct.js exposes base classes and Room to code editors +* Navigate through catnip in-place block search with arrow keys +* Remember last used tileset in an edited room +* Tweak styles of menus a bit so they don't change the height of a menu item when hovered +* Use fixed port number for in-editor docs and debugger so that localStorage doesn't vanish on ct.js restart. Also fixes memory leak with lingering web servers after closing a project. +* Use UI theme colors in room editor's tile picker +* Widen the asset confirmation dialog a bit +* :globe_with_meridians: Update debug and comments translation files +* :globe_with_meridians: Update Russian translation files +* :globe_with_meridians: Update Turkish translation file (by @Sarpmanon) + +### 🐛 Bug Fixes + +* :bento: Fix sound recorder by replacing microm package with @tscole/mic-recorder-to-mp3 +* Add missing translation keys for actions +* Fetch patrons list on devSetup so there're no cache errors while developing locally +* Fix backgrounds blocking click events on copies and tiles +* Fix ct.transition causing an infinite recursion of room removal in its transitions +* Fix Ctrl+G hotkey in the room editor +* Fix mutators making broken blocks if a new `blocks` piece was introduced in a target block. Fixes errors with If-Else blocks that were mutated from If blocks +* Fix nested copies not being removed from appended/prepended rooms when a user calls `rooms.remove` on them. +* Fix not being able to port v3 versions to v5 (fixes incorrect sound conversion) +* Fix UI rooms positioned in reverse coordinate system when using this.x, this.y instead of this.position.x, this.position.y +* Importing a texture from a Buffer must prompt a user for a texture name + +### 🌐 Website + +* :bento: Resolve some npm audit warnings +* :bug: Fix misaligned icons in the navbar +* :zap: Add a Github link to the navbar +* :zap: Add a warning about shitty antiviruses and put a GH issue link for users to report about these cases +* :zap: Automate the changelog page by fetching the release notes from Github +* :zap: Support github issues and users mentions in markdown (for the changelog page) +* :zap: Update Japanese translation (by @taxi13245) + - Clarified ambiguous expressions. + - Added translations. + ## v5.0.1 *Sat Jun 15 2024* From d5c64dfcab212dcc592a032c0d28bce7477e8c1b Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Sat, 20 Jul 2024 18:51:48 +1200 Subject: [PATCH 44/44] :globe_with_meridians: Update Russian UI translation --- app/data/i18n/Russian.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 9cf5a83d1..fc620f421 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -1646,7 +1646,8 @@ "verticalPadding": "Вертикальная отбивка, в пикс.", "horizontalSpacing": "Отступ элементов по горизонтали, в пикс.", "verticalSpacing": "Отступ элементов по вертикали, в пикс.", - "alignItems": "Выравнивать элементы:" + "alignItems": "Выравнивать элементы:", + "pixelPerfectScroll": "Попиксельный скролл" }, "assetInput": { "changeAsset": "Нажмите, чтобы заменить ассет", @@ -1832,5 +1833,11 @@ "enumEditor": { "addVariant": "Добавить вариант", "enumUseCases": "Это перечисление будет доступно во всём коде твоего проекта и будет выступать как тип данных в типах контента и пользовательских полях поведений." + }, + "globalSearch": { + "buttonTitle": "Поиск по ассетам", + "nothingFound": "Ничего не найдено.", + "searchHint": "Поиск по всем ассетам по имени. Недавние ассеты тоже будут тут показаны.", + "recent": "недавно открыто" } }