diff --git a/app/Changelog.md b/app/Changelog.md
index b8d9023df..35afde62c 100644
--- a/app/Changelog.md
+++ b/app/Changelog.md
@@ -1,3 +1,47 @@
+## v4.0.2
+*Sun Feb 25 2024*
+
+### ✨ New Features
+
+* Add a menu item to duplicate emitter tandems. Closes #498
+* Export and import emitter tandems from files
+* Now you can change the background color in the style editor
+
+### ⚡️ General Improvements
+
+* :globe_with_meridians: Update Chinese Simplified translations (#500 by @emaoshushu)
+* Behaviors will now have an additional icon in the asset viewer showing the asset type it was created for
+* Do update checks at max once in an hour if it was successfully requested before
+* Emitter editors will now show a (?) image instead of a blank broken image if no texture was set
+* (Internal) Move properties assignment for AnimatedSprite from the Copy mixin into the pixi.js-based class.
+* Now line height in style editors will scale with the font size when you change the latter
+* (Internal) Refactor copy creation off base classes, move CopyAnimatedSprite, CopyText, CopyContainer prototypes into separate pixi.js-based classes.
+* Remember whether the grid was disabled in a specific room
+* Transparent background in style editor. Make the preview occupy the available space in the editor.
+
+### 🐛 Bug Fixes
+
+* Fix broken QWERTY and Shift+S hotkeys in room editors
+* Fix buttons skipping their pointer events after being disabled and enabled back
+* Fix discardio not removing old keys from asset object before assigning new ones, which led, for example, to not being able to disable stroke/fill/shadow settings of style assets
+* Fix double caching of tile layers that breaks `rooms.merge` call. Closes #501
+* Fix `fs` catmod failing when run in node.js context and trying to work with relative paths
+* Fix hotkeys being ignored if non-english keyboard layout was in use
+* Fix nine slice panes' tint being reset on click and not saved in the room-editor
+* Fix not being able to deselect items after sorting *and* moving them
+* Fix not being able to set tint with `this.tint` or the room editor to buttons.
+* Fix scripts sometimes having blank screen when switching back and forth tabs
+* Remove deleted behaviors from opened template editors, and deleted behaviors and templates links from rooms' properties panel
+* Sound assets should prompt for a name when created
+
+### 📝 Docs
+
+* :bug: Fix movement by a grid (#132 by @0xFFAAF)
+
+### 🌐 Website
+
+* :zap: Update wording on essentials on the homepage
+
## v4.0.1
*Sun Feb 18 2024*
diff --git a/app/data/ct.libs/fs/index.js b/app/data/ct.libs/fs/index.js
index 9a375fa94..9a694a08b 100644
--- a/app/data/ct.libs/fs/index.js
+++ b/app/data/ct.libs/fs/index.js
@@ -247,18 +247,19 @@ try {
const getPath = dest => {
dest = normalize(dest);
- const absoluteDest = path.isAbsolute(dest) ? dest : path.join(fs.gameFolder, dest);
+ const absoluteDest = normalize(path.isAbsolute(dest) ?
+ dest :
+ path.join(fs.gameFolder, dest));
if (fs.forceLocal) {
if (absoluteDest.indexOf(fs.gameFolder) !== 0) {
throw new Error('[fs] Operations outside the save directory are not permitted by default due to safety concerns. If you do need to work outside the save directory, change `fs.forceLocal` to `false`. ' +
- `The save directory: "${fs.gameFolder}", the target directory: "${dest}", which resolves into "${absoluteDest}".`);
+ `The save directory: "${fs.gameFolder}", the target item: "${dest}", which resolves into "${absoluteDest}".`);
}
}
return absoluteDest;
};
const ensureParents = async dest => {
const parents = path.dirname(getPath(dest));
- console.log(parents);
await fsNative.mkdir(getPath(parents), {
recursive: true
});
diff --git a/app/data/i18n/Brazilian Portuguese.json b/app/data/i18n/Brazilian Portuguese.json
index 568342506..88736d008 100644
--- a/app/data/i18n/Brazilian Portuguese.json
+++ b/app/data/i18n/Brazilian Portuguese.json
@@ -205,7 +205,7 @@
"assetGallery": "Galeria de ativos integrada",
"behaviorTemplate": "Compartamento para templates",
"behaviorRoom": "Comportamento para salas",
- "behaviorImport": "Importar do arquivo",
+ "importFromFile": "Importar do arquivo",
"behaviorMissingCatmods": "Seu projeto está faltando estes catmods: $1. Habilite-os primeiro.",
"formatError": "O arquivo aberto está com formato malformado ou, talvez, tenha sido feito para uma versão incompatível do ct.js."
},
diff --git a/app/data/i18n/Chinese Simplified.json b/app/data/i18n/Chinese Simplified.json
index d5d58a329..db7d7c54a 100644
--- a/app/data/i18n/Chinese Simplified.json
+++ b/app/data/i18n/Chinese Simplified.json
@@ -118,12 +118,50 @@
"骨骼精灵图形",
"骨骼精灵图形",
"骨骼精灵图形"
+ ],
+ "behavior": [
+ "行为",
+ "行为",
+ "行为"
+ ],
+ "script": [
+ "脚本",
+ "脚本",
+ "脚本"
]
},
"next": "下一个",
"previous": "上一个",
"undo": "撤销",
- "redo": "恢复"
+ "redo": "恢复",
+ "addBehavior": "添加一个行为",
+ "copyNamesList": "将名字复制成一个列表",
+ "copyNamesArray": "将名称复制为数组元素",
+ "create": "创建",
+ "discard": "丢弃",
+ "experimentalFeature": "这是一个实验性的功能.",
+ "goBack": "返回",
+ "sortByDate": "按日期排序",
+ "sortByName": "按名称排序",
+ "sortByType": "按类型排序",
+ "reimportSourceMissing": "无法重新导入: 源文件现在丢失了.",
+ "dropToImport": "拖到这里导入",
+ "reimport": "重新导入",
+ "alignModes": {
+ "left": "靠左",
+ "right": "靠右",
+ "top": "靠上",
+ "bottom": "靠下",
+ "center": "居中",
+ "topLeft": "居左上",
+ "topCenter": "居中上",
+ "topRight": "居右上",
+ "bottomLeft": "居左下",
+ "bottomCenter": "居中下",
+ "bottomRight": "居右下",
+ "fill": "填满",
+ "Scale": "缩放"
+ }
},
"colorPicker": {
"current": "新建",
@@ -172,14 +210,23 @@
"downloadAndroidStudio": "下载 Android Studio",
"requiresInternetNotice": "此操作需要Internet连接来设置每个项目",
"noJdkFound": "没有找到JDK 17 (JAVA HOME环境变量没有设置或没有指向JDK 17). 你可以在这里获得JDK 17:",
- "downloadJDK": "下载 JDK 17"
+ "downloadJDK": "下载 JDK 17",
+ "goodToGo": "准备好了! 👍",
+ "nodeJsNotFound": "在您的系统中找不到Node.js.",
+ "nodeJsDownloadPage": "下载 Node.js",
+ "nodeJsIcons": "Node.js是可选的,但需要修补Windows可执行文件以添加元数据和图标. 如果你没有安装node.js,你的游戏在Windows上就不会有合适的图标."
},
"intro": {
"loading": "请稍等: 小猫正在聚集光速!",
"newProject": {
"input": "项目名称 (字母和数字)",
"selectProjectFolder": "选择存储项目的文件夹",
- "nameError": "项目名称错误"
+ "nameError": "项目名称错误",
+ "header": "新增",
+ "projectName": "名称:",
+ "language": "编程语言:",
+ "saveFolder": "项目文件夹:",
+ "languageError": "您必须指定一种编程语言"
},
"recovery": {
"message": "
恢复
ct.js已找到恢复文件. 可能是您的项目未正确保存或在紧急情况下ct.js已关闭. 这是这些文件的最后修改时间:
您选择的文件:{0} {1}
恢复文件: {2} {3}
ct.js应该打开什么文件?
",
@@ -209,7 +256,8 @@
"templatesInfo": "你可以通过使用这些模板之一来启动游戏开发.它们只包含占位符图像,但具有有效的机制. 选择项目将为新项目打开一个保存目录选择器",
"boosty": "在 Boosty 上支持 ct.js!",
"sponsoredBy": "自豪地赞助 $1!",
- "supportedBy": "支持了 $1!"
+ "supportedBy": "支持了 $1!",
+ "nothingToShowFiller": "这里没有什么可展示的! 尝试下面的示例或创建您自己的项目."
},
"onboarding": {
"hoorayHeader": "赞! 您成功创建了一个项目!",
@@ -263,7 +311,11 @@
"splashScreen": "启动画面:",
"splashScreenNotice": "该图像将用于移动端构建. 它应该至少是1920x1920像素大小, 并将调整大小和裁剪图像和横向屏幕方向, 所以确保所有重要的东西都在画面中心",
"forceSmoothIcons": "不论渲染设置如何使用平滑的图标",
- "forceSmoothSplashScreen": "不管渲染设置如何使用平滑的启动画面图像"
+ "forceSmoothSplashScreen": "不管渲染设置如何使用平滑的启动画面图像",
+ "alternativeCtjsLogo": "使用替代的 ct.js 标志",
+ "alternativeCtjsNotice": "将启动屏幕上的 \"Made with ct.js\" 替换为常规的ct.js标志. 当你的经销商是个混蛋时很有用.",
+ "customLoadingText": "自定义加载文本",
+ "customLoadingTextHint": "如果想隐藏加载文本, 可以使用空格(不是空字符串)."
},
"modules": {
"heading": "Cat模组"
@@ -283,13 +335,30 @@
},
"pixelatedRender": "在此处和导出的项目中禁用图像平滑 (保留清晰像素)",
"usePixiLegacy": "添加一个传统的, 基于画布的渲染器来支持旧的浏览器和显卡(你的游戏增加~20kb)",
- "useTransparent": "",
+ "useTransparent": "使用透明的PIXI背景",
"mobileBuilds": "移动端构建",
"screenOrientation": "屏幕方向:",
"screenOrientations": {
"unspecified": "任何",
"landscape": "横向",
"portrait": "纵向"
+ },
+ "viewportMode": "视口模式",
+ "viewportModes": {
+ "asIs": "不受控",
+ "fastScale": "快速缩放",
+ "fastScaleInteger": "按整数快速缩放",
+ "expand": "扩展",
+ "scaleFit": "带黑边质量缩放",
+ "scaleFill": "无黑边质量缩放"
+ },
+ "viewportModesDescriptions": {
+ "asIs": "禁用任何视口管理; 渲染的画布将被放置在左上角.",
+ "fastScale": "在不改变分辨率的情况下, 视口将按比例填充屏幕.",
+ "fastScaleInteger": "视口将位于屏幕中间, 并按整数(x2, x3, x4等)进行缩放.",
+ "expand": "视口将填满整个屏幕. 相机将扩大以适应新的区域.",
+ "scaleFit": "视口将按比例填充屏幕, 在基本视口周围留下黑边. 将分辨率更改为与屏幕匹配.",
+ "scaleFill": "视口填满了屏幕, 扩展了相机以避免黑边. 将分辨率更改为与屏幕匹配."
}
},
"scripts": {
@@ -310,7 +379,12 @@
"none": "无",
"minify": "最小化亚索",
"obfuscate": "模糊混淆"
- }
+ },
+ "assetTree": "资产树",
+ "assetTreeNote": "你可以在游戏运行时中将资产树导出为res.tree, 但它也会显示你的项目结构, 并为导出的项目增加一些权重.",
+ "exportAssetTree": "导出资产树",
+ "exportAssetTypes": "只导出这些资产类型:",
+ "autocloseDesktop": "当用户按下 \"关闭\" 按钮时退出应用程序."
},
"catmodsSettings": "Cat模组设置",
"content": {
@@ -333,9 +407,15 @@
"confirmDeletionMessage": "您确定要删除此内容类型吗? 它是不可逆的, 并且也将删除该内容类型的所有条目.",
"gotoEntries": "进入条目",
"entries": "条目",
- "array": "数组"
+ "array": "数组",
+ "fixedLength": "固定长度"
},
- "contentTypes": "内容类型"
+ "contentTypes": "内容类型",
+ "main": {
+ "heading": "主要设置",
+ "miscHeading": "其他设置",
+ "backups": "需要保留的备份文件数量:"
+ }
},
"modules": {
"author": "这个cat模组的作者",
@@ -438,7 +518,8 @@
"spawnType": "形态类型:",
"spawnShapes": {
"rectangle": "矩形",
- "star": "星形"
+ "star": "星形",
+ "torus": "环形"
},
"width": "宽度:",
"height": "高度:",
@@ -454,7 +535,32 @@
"alreadyHasAnImportingTexture": "你已经有了一个名为 $1 的纹理。要么删除,要么重命名;尽管你必须导入与之前添加的相同的纹理 :)",
"changeGrid": "设置网格大小",
"newGridSize": "新的网格大小:",
- "setPreviewTexture": "设置预览纹理"
+ "setPreviewTexture": "设置预览纹理",
+ "textureMethod": "使用多个帧作为:",
+ "textureMethods": {
+ "random": "随机",
+ "animated": "动画"
+ },
+ "animatedFramerate": "帧率:",
+ "easingHeader": "缓冲",
+ "easing": {
+ "none": "阶梯式",
+ "linear": "线性",
+ "smooth": "平滑"
+ },
+ "movementType": "运动类型:",
+ "movementTypes": {
+ "linear": "线性",
+ "accelerated": "使用重力"
+ },
+ "rotateTexture": "沿着运动方向旋转纹理",
+ "rotationMethod": "纹理旋转:",
+ "rotationMethods": {
+ "static": "固定的",
+ "dynamic": "动态的"
+ },
+ "burstSpacing": "光线之间的间距, 以度为单位:",
+ "burstStart": "起始方向, 以度为单位:"
},
"rooms": {
"create": "添加新的",
@@ -465,7 +571,8 @@
"global": "全局记事本",
"backToHome": "回到文档主页",
"modulesPages": "模组文档",
- "helpPages": "帮助"
+ "helpPages": "帮助",
+ "docsAndNotes": "文档 & 笔记"
},
"patreon": {
"aboutPatrons": "赞助人是以经常性捐赠的形式表示对Patreon的ComigoGames的支持. 并不是每个人都来自ct.js. 有些是正在使用ComigoGames的其他应用程序. 提示: 如果您是创作者, 并通过Patreon捐赠给ComigoGames, 您将在此处找到指向您页面的链接 — 这就是我对您的创作的小小帮助 :)",
@@ -508,7 +615,10 @@
"systemInfoWait": "等一下, 我正在收集数据…",
"systemInfoDone": "完成!",
"disableAcceleration": "禁用图形加速(需要重启)",
- "postAnIssue": "在Github上发布issue"
+ "postAnIssue": "在Github上发布issue",
+ "disableVulkan": "禁用Vulkan支持",
+ "disableVulkanSDHint": "修复了SteamDeck和其他一些Linux系统上 \"不支持WebGL\" 的问题. 需要重启才能生效.",
+ "restartMessage": "请重新启动应用程序以应用更改."
},
"deploy": {
"exportDesktop": "导出到桌面",
@@ -553,13 +663,15 @@
"project": {
"heading": "项目",
"startScreen": "返回启动画面",
- "startNewWindow": "",
+ "startNewWindow": "打开新项目窗口",
"successZipProject": "已成功将项目压缩到{0}.",
"save": "保存项目",
"zipProject": "将项目打包到.zip",
"openIncludeFolder": "打开\"include\"文件夹",
"openProject": "打开项目…",
- "openExample": "打开示例项目…"
+ "openExample": "打开示例项目…",
+ "convertToJs": "将项目转换为JavaScript",
+ "confirmationConvertToJs": "这将自动将项目中的所有事件转换为JavaScript. 这种行为是不可逆转的. (但如果在脚本中发现错误, 它将回滚.) 您确定要将这个CoffeeScript项目转换为JavaScript吗?"
},
"meta": {
"license": "许可证",
@@ -608,7 +720,14 @@
"tabMainMenuMeta": "如果以后需要帮助, 您可以在元数据面板中找到所有的官方hub.",
"helpPanelReminder": "还有别忘了内置的文档! 我们建议开始您自己的项目前先完成官方教程.",
"buttonStartTutorial": "打开教程"
- }
+ },
+ "assets": "资产",
+ "applyAssetsQuestion": "启动前应用资产吗?",
+ "applyAssetsExplanation": "有些资产没有被应用, 它们的变化不会反映在导出的项目中. 你想现在就申请吗?",
+ "unsavedAssets": "未保存的资产:",
+ "runWithoutApplying": "依然启动",
+ "applyAndRun": "应用并运行",
+ "cantAddEditor": "不能添加其他编辑器. 请关闭一些带有空间, 样式或串联编辑器的选项卡."
},
"docsPanel": {
"documentation": "文档",
@@ -660,7 +779,16 @@
"folderNotWritable": "您没有权限写入这个文件夹. 选择其他试试",
"complete": "文件夹已设置, 一切正常✅"
},
- "assetViewer": {},
+ "assetViewer": {
+ "root": "根",
+ "toggleFolderTree": "切换文件夹树可见性",
+ "addNewFolder": "新建文件夹",
+ "newFolderName": "新文件夹",
+ "unwrapFolder": "打开文件夹",
+ "confirmDeleteFolder": "您确定要删除此文件夹吗? 它的所有内容也将被删除.",
+ "confirmUnwrapFolder": "您确定要打开此文件夹吗? 它的所有内容都将放在当前文件夹中.",
+ "exportBehavior": "导出这个行为"
+ },
"soundRecorder": {
"recorderHeading": "录音机",
"record": "开始录音",
@@ -687,7 +815,10 @@
"show": "显示图层",
"hide": "隐藏图层",
"findTileset": "查找图块集",
- "addTileLayer": "添加一个平铺层"
+ "addTileLayer": "添加一个平铺层",
+ "addTileLayerFirst": "首先在左侧面板添加一个贴图层!",
+ "cacheLayer": "缓存这个图层",
+ "cacheLayerWarning": "缓存极大地加快了贴图层的渲染速度. 只有当你需要在游戏过程中动态改变这个贴图层时, 你才应该禁用这个选项."
},
"roomView": {
"name": "名称:",
@@ -760,9 +891,55 @@
"addCopies": "添加副本",
"addTiles": "添加图块集",
"manageBackgrounds": "管理背景",
- "roomProperties": "房间属性"
+ "roomProperties": "房间属性",
+ "uiTools": "UI 工具"
},
- "resetView": "重置视角"
+ "resetView": "重置视角",
+ "viewportHeading": "视口",
+ "followTemplate": "遵循一个模板:",
+ "followCodeHint": "了解如何用代码进一步调整此特性",
+ "sendToBack": "发送到后面",
+ "sendToFront": "发送到前面",
+ "emptyTextFiller": "<空>",
+ "uiTools": {
+ "noSelectionNotice": "选择副本以对其对齐或更改其文本标签.",
+ "textSettings": "文本设置",
+ "customText": "文本",
+ "customTextSize": "字体大小",
+ "wordWrapWidth": "以这个宽度环绕",
+ "textAlignment": "对齐方式",
+ "alignmentSettings": "自动对齐",
+ "enableAutoAlignment": "启用自动对齐",
+ "autoAlignHint": "当打开时, 此元素将自动对齐自己相对于房间的指定部分的边界, 定义为 \"框架\" . 当使用扩展和质量缩放而不使用信箱渲染模式时, 它是有用的, 因为相机会在这些视口模式中改变比例.",
+ "frame": "帧位置,以%为单位:",
+ "framePadding": "帧填充, 以像素为单位:",
+ "innerFrameMarker": "内框",
+ "outerFrameMarker": "外框",
+ "constrains": "约束:",
+ "constrainsTooltips": {
+ "left": "将间隙的大小锁定在框架的左侧",
+ "right": "将间隙的大小锁定在框架的右侧",
+ "top": "将间隙的大小锁定在框架的顶部",
+ "bottom": "将间隙的大小锁定在框架的底部",
+ "centerVertical": "放置垂直相对于中心的框架",
+ "centerHorizontal": "放置水平相对于中心的框架"
+ },
+ "bindings": "绑定",
+ "bindingsHelp": "您可以在这里编写JavaScript表达式, 它将在每一帧中求值, 并将更新副本的相应属性.",
+ "bindingNames": {
+ "text": "文本:",
+ "disabled": "禁用:",
+ "visible": "可见性:",
+ "tex": "纹理:",
+ "tint": "色调:",
+ "count": "精灵数量:"
+ },
+ "bindingTypes": {
+ "string": "字符串值",
+ "boolean": "布尔值(true或false)",
+ "number": "数字"
+ }
+ }
},
"styleView": {
"active": "激活",
@@ -839,7 +1016,11 @@
"marginY": "边距 Y:",
"offX": "位移 X:",
"offY": "位移 Y:",
- "blankTextureNotice": "将图像导出为透明矩形, 因此在游戏中不可见. 用于为ct.js编辑器设置占位符, 同时保持包的大小较轻."
+ "blankTextureNotice": "将图像导出为透明矩形, 因此在游戏中不可见. 用于为ct.js编辑器设置占位符, 同时保持包的大小较轻.",
+ "slicing": "切片",
+ "viewSettings": "查看设置",
+ "exportSettings": "导出设置",
+ "axisExplanation": "定义哪个位置被计数为副本的(0;0)位置, 并影响它相对于网格的位置, 以及它围绕哪个点旋转."
},
"fontView": {
"italic": "是否斜体?",
@@ -898,7 +1079,42 @@
"screen": "遮蔽 (变亮)"
},
"animationFPS": "动画 FPS:",
- "loopAnimation": "循环动画"
+ "loopAnimation": "循环动画",
+ "hoverTexture": "悬停时纹理",
+ "pressedTexture": "按下时纹理",
+ "disabledTexture": "禁用时纹理",
+ "defaultText": "默认文本:",
+ "textStyle": "文本样式",
+ "fieldType": "字段类型",
+ "fieldTypes": {
+ "text": "文本",
+ "number": "数字",
+ "email": "邮件",
+ "password": "密码"
+ },
+ "useCustomSelectionColor": "为选定的文本使用自定义颜色",
+ "maxLength": "最大长度:",
+ "baseClass": {
+ "AnimatedSprite": "动画精灵",
+ "Text": "文本",
+ "NineSlicePlane": "面板",
+ "Container": "容器",
+ "Button": "按钮",
+ "RepeatingTexture": "重复纹理",
+ "SpritedCounter": "精灵计数器",
+ "TextBox": "文本盒子"
+ },
+ "nineSliceTop": "顶部切片, 以像素为单位",
+ "nineSliceRight": "右侧切片, 以像素为单位",
+ "nineSliceBottom": "底部切片, 以像素为单位",
+ "nineSliceLeft": "左侧切片, 以像素为单位",
+ "autoUpdateNineSlice": "自动更新碰撞形状",
+ "autoUpdateNineSliceHint": "如果一个面板改变了它的大小, 它将自动更新它的碰撞形状. 对于纯粹的装饰性元素, 或者那些在创建后从未改变其大小的元素, 通常不需要这样做. 你仍然可以在任何时候用 u.reshapeNinePatch(this) 调用更新它的碰撞形状.",
+ "panelHeading": "纹理切片设置",
+ "scrollSpeedX": "X轴滚动速度:",
+ "scrollSpeedY": "Y轴滚动速度:",
+ "isUi": "使用UI时间",
+ "defaultCount": "默认精灵数:"
},
"assetInput": {
"changeAsset": "切换资产",
@@ -933,7 +1149,9 @@
"pointer": "指针事件",
"misc": "杂项",
"animation": "动画",
- "timers": "计时器"
+ "timers": "计时器",
+ "input": "输入",
+ "app": "应用"
},
"coreEvents": {
"OnCreate": "创建时",
@@ -956,7 +1174,11 @@
"OnFrameChange": "帧改变",
"OnAnimationLoop": "动画循环",
"OnAnimationComplete": "动画完成",
- "Timer": "计时器 $1"
+ "Timer": "计时器 $1",
+ "OnAppFocus": "应用运行中",
+ "OnAppBlur": "应用在后台",
+ "OnTextChange": "文本改变时",
+ "OnTextInput": "文本输入时"
},
"coreParameterizedNames": {
"OnActionPress": "当 %%action%% 按下",
@@ -976,7 +1198,9 @@
},
"coreEventsLocals": {
"OnActionDown_value": "当前动作的值",
- "OnActionPress_value": "当前动作的值"
+ "OnActionPress_value": "当前动作的值",
+ "OnTextChange_value": "新的文本值",
+ "OnTextInput_value": "新的文本值"
},
"coreEventsDescriptions": {
"OnCreate": "在创建副本时触发.",
@@ -990,7 +1214,91 @@
"OnActionDown": "如果动作的输入是活动的, 则运行每一帧.",
"OnAnimationLoop": "每次动画重新启动时触发.",
"OnAnimationComplete": "非循环动画结束时触发.",
- "Timer": "用这个设置定时器的持续时间, 单位为秒. 定时器$1 = 3;"
- }
+ "Timer": "用这个设置定时器的持续时间, 单位为秒. 定时器$1 = 3;",
+ "OnAppFocus": "当用户返回到您的应用程序时触发.",
+ "OnAppBlur": "当用户从你的游戏切换到其他 — 通过切换标签, 切换到另一个窗口, 或最小化游戏时触发.",
+ "OnTextChange": "当用户通过单击该字段外部或按 Enter 键完成对该字段的编辑时触发.",
+ "OnTextInput": "每当用户更改此字段的值时触发."
+ },
+ "jumpToProblem": "跳转到问题处",
+ "staticEventWarning": "此事件使此行为成为静态的. 你将无法在游戏中动态添加或删除它的行为API, 但除此之外, 它将是完全可用的.",
+ "restrictedEventWarning": "此事件只适用于具有以下基类的模板: $1. 当应用于其他基类的模板时, 此事件将不起作用.",
+ "baseClassWarning": "此事件不适用于当前基类"
+ },
+ "assetConfirm": {
+ "confirmHeading": "选择动作",
+ "confirmParagraph": "$1 资产有未应用的更改. 你想用它做什么?"
+ },
+ "behaviorEditor": {
+ "customFields": "自定义字段",
+ "customFieldsDescription": "自定义字段可用于向资产编辑器添加额外的输入. 这些字段将在属性面板中可用, 并且可以作为 \"this\" 对象的属性使用. 例如, 如果您创建了一个名称为 \"sausage\" 的字段, 您可以在事件中将其读取为 \"this.sausage\"."
+ },
+ "createAsset": {
+ "newAsset": "新的资产",
+ "placeholderTexture": "占位纹理",
+ "assetGallery": "内置资产库",
+ "behaviorTemplate": "模板可用行为",
+ "behaviorRoom": "房间可用行为",
+ "importFromFile": "从文件导入",
+ "behaviorMissingCatmods": "您的项目缺少这些catmods: $1. 首先启用它们.",
+ "formatError": "打开的文件格式不正确, 或者可能是为不兼容的ct.js版本制作的."
+ },
+ "exporterError": {
+ "exporterErrorHeader": "导出项目时发生错误",
+ "errorSource": "$1 调用 $2",
+ "clueSolutions": {
+ "syntax": "这是代码中的语法错误. 转到资产并修复它 — 代码编辑器将突出显示有问题的地方.",
+ "eventConfiguration": "其中一个事件配置错误, 字段为空. 转到资源并编辑其事件的参数.",
+ "emptySound": "您的一个声音没有附加任何声音文件. 将声音文件导入到它或删除此空声音.",
+ "emptyEmitter": "你的一个粒子系统发射器的纹理丢失了. 你需要为它设置纹理或者移除发射器.",
+ "windowsFileLock": "这是一个windows特有的锁定文件问题. 确保你已经关闭了所有启动游戏的外部浏览器, 然后再次尝试导出. 如果没有帮助, 重新启动ct.js.",
+ "noTemplateTexture": "其中一个模板缺少纹理. 你需要为它设置纹理."
+ },
+ "stacktrace": "调用栈",
+ "jumpToProblem": "跳转到问题处",
+ "saveAndQuit": "保存和退出"
+ },
+ "folderEditor": {
+ "title": "文件夹设置",
+ "icon": "图表:",
+ "color": "颜色:"
+ },
+ "languageSelector": {
+ "chooseLanguageHeader": "选择你的编程语言",
+ "chooseLanguageExplanation": "这是你编写事件来描述游戏玩法逻辑的语言. 以前, 所有的项目都使用JavaScript+TypeScript. 请注意, 您只能将CoffeeScript项目转换为JavaScript, 反过来则不行, 因此请谨慎选择!",
+ "coffeeScriptDescription": "语法简单, 适合初学者的好语言. 如果您之前没有代码编程的经验, 或者您喜欢Python, 请选择这种语言.",
+ "pickCoffeeScript": "我选择CoffeeScript!",
+ "jsAndTs": "JavaScript (以及 TypeScript)",
+ "jsTsDescription": "web语言. 它的语法更复杂, 但它有编辑器内的错误高亮显示和代码建议. 如果你以前使用过JS, C#或Java代码, 请选择它.",
+ "pickJsTs": "我选择JavaScript!",
+ "acceptAndSpecifyDirectory": "接受并选择项目文件夹"
+ },
+ "newAssetPrompt": {
+ "heading": "创建新资产",
+ "selectNewName": "新资产名称:"
+ },
+ "scriptView": {
+ "runAutomatically": "在游戏开始时执行",
+ "language": "语言:",
+ "convertToJavaScript": "转换为JavaScript"
+ },
+ "soundView": {
+ "variants": "变奏",
+ "addVariant": "从文件中添加…",
+ "preload": "游戏开始时加载",
+ "volume": "音量",
+ "pitch": "音高",
+ "distortion": "失真",
+ "effects": "效果",
+ "reverb": "混响",
+ "reverbDuration": "时长",
+ "reverbDecay": "衰变",
+ "reverseReverb": "反转",
+ "equalizer": "均衡器",
+ "hertz": "赫兹",
+ "positionalAudio": "3D 音效",
+ "falloff": "衰减:",
+ "refDistance": "衰减开始:",
+ "positionalAudioHint": "这只影响 sounds.playAt 方法. 衰减设置了声音消失的速度, 0表示没有衰减, 大值表示几乎立即下降到沉默. 衰减开始指的是声音开始消失的距离, 1为屏幕的一半."
}
}
diff --git a/app/data/i18n/Comments.json b/app/data/i18n/Comments.json
index e392f2f4a..aeb2a44a2 100644
--- a/app/data/i18n/Comments.json
+++ b/app/data/i18n/Comments.json
@@ -1239,7 +1239,7 @@
"assetGallery": "",
"behaviorTemplate": "",
"behaviorRoom": "",
- "behaviorImport": "",
+ "importFromFile": "",
"behaviorMissingCatmods": "",
"formatError": ""
},
@@ -1301,4 +1301,4 @@
"refDistance": "",
"positionalAudioHint": ""
}
-}
\ No newline at end of file
+}
diff --git a/app/data/i18n/Debug.json b/app/data/i18n/Debug.json
index 703cf96ae..c29c4ede8 100644
--- a/app/data/i18n/Debug.json
+++ b/app/data/i18n/Debug.json
@@ -1239,7 +1239,7 @@
"assetGallery": "createAsset.assetGallery",
"behaviorTemplate": "createAsset.behaviorTemplate",
"behaviorRoom": "createAsset.behaviorRoom",
- "behaviorImport": "createAsset.behaviorImport",
+ "importFromFile": "createAsset.importFromFile",
"behaviorMissingCatmods": "createAsset.behaviorMissingCatmods",
"formatError": "createAsset.formatError"
},
@@ -1301,4 +1301,4 @@
"refDistance": "soundView.refDistance",
"positionalAudioHint": "soundView.positionalAudioHint"
}
-}
\ No newline at end of file
+}
diff --git a/app/data/i18n/Dutch.json b/app/data/i18n/Dutch.json
index ef5187f3f..37f52d696 100644
--- a/app/data/i18n/Dutch.json
+++ b/app/data/i18n/Dutch.json
@@ -1240,7 +1240,7 @@
"assetGallery": "Ingebouwde asset gallerij",
"behaviorTemplate": "Gedrag voor templates",
"behaviorRoom": "Gedrag voor kamers",
- "behaviorImport": "Uit een bestand importeren",
+ "importFromFile": "Uit een bestand importeren",
"behaviorMissingCatmods": "Jouw project mist deze catmods: $1. Activeer ze eerst.",
"formatError": "Het geopende bestand heeft is slechtgevormd of, misschien was het bedoeld voor een incompatibele versie van ct.js."
},
diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json
index 53498014c..9d0b7b4ef 100644
--- a/app/data/i18n/English.json
+++ b/app/data/i18n/English.json
@@ -180,7 +180,8 @@
"unwrapFolder": "Unwrap the folder",
"confirmDeleteFolder": "Are you sure you want to delete this folder? All its contents will be deleted as well.",
"confirmUnwrapFolder": "Are you sure you want to unwrap this folder? All its contents will be placed in the current folder.",
- "exportBehavior": "Export this behavior"
+ "exportBehavior": "Export this behavior",
+ "exportTandem": "Export this emitter tandem"
},
"behaviorEditor": {
"customFields": "Custom Fields",
@@ -213,7 +214,7 @@
"assetGallery": "Built-in asset gallery",
"behaviorTemplate": "Behavior for templates",
"behaviorRoom": "Behavior for rooms",
- "behaviorImport": "Import from file",
+ "importFromFile": "Import from file",
"behaviorMissingCatmods": "Your project is missing these catmods: $1. Enable them first.",
"formatError": "The opened file has a malformed format or, perhaps, was made for an incompatible version of ct.js."
},
@@ -1301,4 +1302,4 @@
"isUi": "Use UI time",
"defaultCount": "Default sprite count:"
}
-}
\ No newline at end of file
+}
diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json
index fdc4dfcdd..0e2f60cbb 100644
--- a/app/data/i18n/Russian.json
+++ b/app/data/i18n/Russian.json
@@ -802,7 +802,8 @@
"unwrapFolder": "Вывернуть папку",
"confirmDeleteFolder": "Ты точно хочешь удалить эту папку? Её содержимое будет тоже удалено.",
"confirmUnwrapFolder": "Ты точно хочешь вывернуть эту папку? Всё её содержимое будет расположено в текущей папке.",
- "exportBehavior": "Экспортировать поведение"
+ "exportBehavior": "Экспортировать поведение",
+ "exportTandem": "Экспортировать этот тандем"
},
"soundRecorder": {
"recorderHeading": "Диктофон",
@@ -1263,7 +1264,7 @@
"placeholderTexture": "Текстура-заглушка",
"behaviorTemplate": "Поведение для шаблонов",
"behaviorRoom": "Поведение для комнат",
- "behaviorImport": "Импортировать из файла",
+ "importFromFile": "Импортировать из файла",
"behaviorMissingCatmods": "В твоём проекте нет следующих котомодов: $1. Сначала включи их.",
"formatError": "Файл неправильного формата или, быть может, был сделан для несовместимой версии ct.js.",
"assetGallery": "Встроенная галерея ассетов"
@@ -1301,4 +1302,4 @@
"language": "Язык:",
"convertToJavaScript": "Конвертировать в JavaScript"
}
-}
\ No newline at end of file
+}
diff --git a/app/data/i18n/Turkish.json b/app/data/i18n/Turkish.json
index d6eb76894..5f0922467 100644
--- a/app/data/i18n/Turkish.json
+++ b/app/data/i18n/Turkish.json
@@ -1212,7 +1212,7 @@
"assetGallery": "Dahili varlık galerisi",
"behaviorTemplate": "Şablonlar için davranış",
"behaviorRoom": "Odalar için davranış",
- "behaviorImport": "Dosyadan içeri aktar",
+ "importFromFile": "Dosyadan içeri aktar",
"behaviorMissingCatmods": "Projende $1 catmodları bulunmuyor. Önce onları etkinleştirmelisin.",
"formatError": "Açılmış dosyada ya kusurlu bir format var ya da, belki, ct.js'in uyumsuz bir sürümü için tasarlandı."
},
diff --git a/app/package-lock.json b/app/package-lock.json
index f8ec9c72a..b1fb24965 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ctjs",
- "version": "4.0.1",
+ "version": "4.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ctjs",
- "version": "4.0.1",
+ "version": "4.0.2",
"license": "MIT",
"dependencies": {
"@capacitor/cli": "^5.5.0",
diff --git a/app/package.json b/app/package.json
index a84bfb089..719d1d932 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": "4.0.1",
+ "version": "4.0.2",
"homepage": "https://ctjs.rocks/",
"author": {
"name": "Cosmo Myzrail Gorynych",
diff --git a/docs b/docs
index 0db0da893..977188ba1 160000
--- a/docs
+++ b/docs
@@ -1 +1 @@
-Subproject commit 0db0da89356937f0546277d09f4557b6cca2c39c
+Subproject commit 977188ba163fc2793b06243d76620178b292eae3
diff --git a/package-lock.json b/package-lock.json
index 5792a6eb3..f499ee8d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ctjsbuildenvironment",
- "version": "4.0.1",
+ "version": "4.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ctjsbuildenvironment",
- "version": "4.0.1",
+ "version": "4.0.2",
"license": "MIT",
"dependencies": {
"@ct.js/gulp-typescript": "^6.0.0",
diff --git a/package.json b/package.json
index f3a8b6a3f..d66bf5c8a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ctjsbuildenvironment",
- "version": "4.0.1",
+ "version": "4.0.2",
"description": "",
"directories": {
"doc": "docs"
diff --git a/src/ct.release/rooms.ts b/src/ct.release/rooms.ts
index 7850e7406..87ad94c78 100644
--- a/src/ct.release/rooms.ts
+++ b/src/ct.release/rooms.ts
@@ -442,7 +442,6 @@ const roomsLib = {
target.tileLayers.push(tl);
target.addChild(tl);
generated.tileLayers.push(tl);
- tl.cache();
}
for (const t of template.objects) {
const c = templatesLib.copyIntoRoom(t.template, t.x, t.y, target, {
diff --git a/src/ct.release/templateBaseClasses/PixiAnimatedSprite.ts b/src/ct.release/templateBaseClasses/PixiAnimatedSprite.ts
new file mode 100644
index 000000000..cac25c237
--- /dev/null
+++ b/src/ct.release/templateBaseClasses/PixiAnimatedSprite.ts
@@ -0,0 +1,28 @@
+import res from '../res';
+import {ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
+
+import type * as pixiMod from 'node_modules/pixi.js';
+declare var PIXI: typeof pixiMod;
+
+export default class PixiAnimateSprite extends PIXI.AnimatedSprite {
+ constructor(t: ExportedTemplate, exts: Record) {
+ if (t?.baseClass !== 'AnimatedSprite') {
+ throw new Error('Don\'t call PixiButton class directly! Use templates.copy to create an instance instead.');
+ }
+ const textures = res.getTexture(t.texture);
+ super(textures);
+ this.anchor.x = t.anchorX ?? textures[0].defaultAnchor.x ?? 0;
+ this.anchor.y = t.anchorY ?? textures[0].defaultAnchor.y ?? 0;
+ this.scale.set(
+ (exts.scaleX as number) ?? 1,
+ (exts.scaleY as number) ?? 1
+ );
+ this.blendMode = t.blendMode || PIXI.BLEND_MODES.NORMAL;
+ this.loop = t.loopAnimation;
+ this.animationSpeed = t.animationFPS / 60;
+ if (t.playAnimationOnStart) {
+ this.play();
+ }
+ return this;
+ }
+}
diff --git a/src/ct.release/templateBaseClasses/PixiButton.ts b/src/ct.release/templateBaseClasses/PixiButton.ts
index 7780e3b3e..bdccfdf05 100644
--- a/src/ct.release/templateBaseClasses/PixiButton.ts
+++ b/src/ct.release/templateBaseClasses/PixiButton.ts
@@ -2,7 +2,7 @@ import stylesLib from '../styles';
import {ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
import resLib from '../res';
import uLib from '../u';
-import {CopyButton} from 'templates';
+import {CopyButton} from '../templateBaseClasses';
import type * as pixiMod from 'node_modules/pixi.js';
declare var PIXI: typeof pixiMod;
@@ -27,7 +27,7 @@ export default class PixiButton extends PIXI.Container {
this.eventMode = 'none';
} else {
this.panel.texture = this.normalTexture;
- this.eventMode = 'auto';
+ this.eventMode = 'dynamic';
}
}
@@ -38,6 +38,16 @@ export default class PixiButton extends PIXI.Container {
this.textLabel.text = val;
}
+ /**
+ * The color of the button's texture.
+ */
+ get tint(): pixiMod.ColorSource {
+ return this.panel.tint;
+ }
+ set tint(val: pixiMod.ColorSource) {
+ this.panel.tint = val;
+ }
+
constructor(t: ExportedTemplate, exts: Record) {
if (t?.baseClass !== 'Button') {
throw new Error('Don\'t call PixiButton class directly! Use templates.copy to create an instance instead.');
diff --git a/src/ct.release/templateBaseClasses/PixiContainer.ts b/src/ct.release/templateBaseClasses/PixiContainer.ts
new file mode 100644
index 000000000..7309df551
--- /dev/null
+++ b/src/ct.release/templateBaseClasses/PixiContainer.ts
@@ -0,0 +1,13 @@
+import type * as pixiMod from 'node_modules/pixi.js';
+declare var PIXI: typeof pixiMod;
+
+export default class PixiContainer extends PIXI.Container {
+ shape: textureShape;
+ constructor() {
+ super();
+ this.shape = {
+ type: 'point'
+ };
+ return this;
+ }
+}
diff --git a/src/ct.release/templateBaseClasses/PixiNineSlicePlane.ts b/src/ct.release/templateBaseClasses/PixiNineSlicePlane.ts
new file mode 100644
index 000000000..f6b1d9829
--- /dev/null
+++ b/src/ct.release/templateBaseClasses/PixiNineSlicePlane.ts
@@ -0,0 +1,40 @@
+import {ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
+import resLib from '../res';
+import uLib from '../u';
+import {CopyPanel} from '../templateBaseClasses';
+
+import type * as pixiMod from 'node_modules/pixi.js';
+declare var PIXI: typeof pixiMod;
+
+export default class PixiPanel extends PIXI.NineSlicePlane {
+ /**
+ * Whether to automatically update the collision shape of this panel
+ * when it changes its size.
+ */
+ updateNineSliceShape: boolean;
+ baseClass = 'NineSlicePlane';
+ constructor(t: ExportedTemplate, exts: Record) {
+ if (t?.baseClass !== 'NineSlicePlane') {
+ throw new Error('Don\'t call PixiPanel class directly! Use templates.copy to create an instance instead.');
+ }
+ const tex = resLib.getTexture(t.texture, 0);
+ super(
+ tex,
+ t.nineSliceSettings?.left ?? 16,
+ t.nineSliceSettings?.top ?? 16,
+ t.nineSliceSettings?.right ?? 16,
+ t.nineSliceSettings?.bottom ?? 16
+ );
+ this.updateNineSliceShape = t.nineSliceSettings.autoUpdate;
+ const baseWidth = this.width,
+ baseHeight = this.height;
+ if ('scaleX' in exts) {
+ this.width = baseWidth * (exts.scaleX as number);
+ }
+ if ('scaleY' in exts) {
+ this.height = baseHeight * (exts.scaleY as number);
+ }
+ uLib.reshapeNinePatch(this as CopyPanel);
+ this.blendMode = t.blendMode || PIXI.BLEND_MODES.NORMAL;
+ }
+}
diff --git a/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts b/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts
index 49fd0d969..4dbf7bf2a 100644
--- a/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts
+++ b/src/ct.release/templateBaseClasses/PixiScrollingTexture.ts
@@ -1,8 +1,8 @@
import {ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
import resLib from '../res';
-import uLib from 'u';
-import roomsLib from 'rooms';
-import {BasicCopy} from 'templates';
+import uLib from '../u';
+import roomsLib from '../rooms';
+import {BasicCopy} from '../templates';
import type * as pixiMod from 'node_modules/pixi.js';
declare var PIXI: typeof pixiMod;
diff --git a/src/ct.release/templateBaseClasses/PixiText.ts b/src/ct.release/templateBaseClasses/PixiText.ts
new file mode 100644
index 000000000..198238614
--- /dev/null
+++ b/src/ct.release/templateBaseClasses/PixiText.ts
@@ -0,0 +1,45 @@
+import {ExportedStyle, ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
+import uLib from '../u';
+import {CopyText} from '.';
+
+import type * as pixiMod from 'node_modules/pixi.js';
+import stylesLib from '../styles';
+declare var PIXI: typeof pixiMod;
+
+export default class PixiText extends PIXI.Text {
+ constructor(t: ExportedTemplate, exts: Record) {
+ if (t?.baseClass !== 'Text') {
+ throw new Error('Don\'t call PixiPanel class directly! Use templates.copy to create an instance instead.');
+ }
+ let style: ExportedStyle;
+ if (t.textStyle && t.textStyle !== -1) {
+ style = stylesLib.get(t.textStyle, true);
+ } else {
+ style = {} as ExportedStyle;
+ }
+ if (exts.customWordWrap) {
+ style.wordWrap = true;
+ style.wordWrapWidth = Number(exts.customWordWrap);
+ }
+ if (exts.customSize) {
+ style.fontSize = Number(exts.customSize);
+ }
+ super(
+ (exts.customText as string) || t.defaultText || '',
+ style as unknown as Partial
+ );
+ if (exts.customAnchor) {
+ const anchor = exts.customAnchor as {
+ x?: number,
+ y?: number
+ };
+ (this as CopyText).anchor.set(anchor?.x ?? 0, anchor?.y ?? 0);
+ }
+ (this as CopyText).shape = uLib.getRectShape(this);
+ (this as CopyText).scale.set(
+ (exts.scaleX as number) ?? 1,
+ (exts.scaleY as number) ?? 1
+ );
+ return this;
+ }
+}
diff --git a/src/ct.release/templateBaseClasses/PixiTextBox.ts b/src/ct.release/templateBaseClasses/PixiTextBox.ts
index 29d0eef7d..a1ef8c199 100644
--- a/src/ct.release/templateBaseClasses/PixiTextBox.ts
+++ b/src/ct.release/templateBaseClasses/PixiTextBox.ts
@@ -2,7 +2,8 @@ import stylesLib from '../styles';
import {ExportedTemplate} from '../../node_requires/exporter/_exporterContracts';
import resLib from '../res';
import uLib from '../u';
-import {BasicCopy, CopyTextBox} from 'templates';
+import {BasicCopy} from 'templates';
+import {CopyTextBox} from 'templateBaseClasses';
import {setFocusedElement} from '../templates';
import {pixiApp, settings as settingsLib} from 'index';
diff --git a/src/ct.release/templateBaseClasses/index.ts b/src/ct.release/templateBaseClasses/index.ts
new file mode 100644
index 000000000..326e530fb
--- /dev/null
+++ b/src/ct.release/templateBaseClasses/index.ts
@@ -0,0 +1,80 @@
+import PixiButton from './PixiButton';
+import PixiSpritedCounter from './PixiSpritedCounter';
+import PixiScrollingTexture from './PixiScrollingTexture';
+import PixiTextBox from './PixiTextBox';
+// import PixiScrollBox from './PixiScrollBox';
+import PixiPanel from './PixiNineSlicePlane';
+import PixiText from './PixiText';
+import PixiContainer from './PixiContainer';
+import PixiAnimatedSprite from './PixiAnimatedSprite';
+
+import {ICopy} from '../templates';
+
+import type * as pixiMod from 'node_modules/pixi.js';
+import {BaseClass} from '../../node_requires/exporter/_exporterContracts';
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+type Constructor = Function & { prototype: T };
+export const baseClassToPixiClass: Record> = {
+ AnimatedSprite: PixiAnimatedSprite,
+ Button: PixiButton,
+ Container: PixiContainer,
+ NineSlicePlane: PixiPanel,
+ RepeatingTexture: PixiScrollingTexture,
+ // ScrollBox: PixiScrollBox,
+ SpritedCounter: PixiSpritedCounter,
+ Text: PixiText,
+ TextBox: PixiTextBox
+};
+
+// Record allows ct.js users to write any properties to their copies
+// without typescript complaining.
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/**
+ * An instance of a ct.js template with Animated Sprite as its base class.
+ * It has functionality of both PIXI.AnimatedSprite and ct.js Copies.
+ */
+export type CopyAnimatedSprite = Record & pixiMod.AnimatedSprite & ICopy;
+/**
+ * An instance of a ct.js template with Panel as its base class.
+ * It has functionality of both PIXI.NineSlicePlane and ct.js Copies.
+ */
+export type CopyPanel = Record & PixiPanel & ICopy;
+/**
+ * An instance of a ct.js template with Text as its base class.
+ * 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 Container as its base class.
+ * It has functionality of both PIXI.Container and ct.js Copies, and though by itself it doesn't
+ * display anything, you can add other copies and pixi.js classes with `this.addChild(copy)`.
+ */
+export type CopyContainer = Record & PixiContainer & ICopy;
+/**
+ * An instance of a ct.js template with button logic.
+ * It has functionality of both PIXI.Container and ct.js Copies.
+ */
+export type CopyButton = Record & PixiButton & ICopy;
+/**
+ * An instance of a ct.js template with text box logic.
+ * It has functionality of both PIXI.Container and ct.js Copies.
+ */
+export type CopyTextBox = Record & PixiTextBox & ICopy;
+/**
+ * An instance of a ct.js template with repeating texture logic.
+ * The texture can expand in any direction and can be animated by scrolling.
+ */
+export type CopyRepeatingTexture = Record & PixiScrollingTexture & ICopy;
+/**
+ * An instance of a ct.js template with a sprited counter logic.
+ * This copy displays a number of identical sprites in a row, similar to sprited healthbars.
+ */
+export type CopySpritedCounter = Record & PixiSpritedCounter & 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 implements a scrollbox that
+ * has a scrollbar and clips its contents.
+ */
+// export type CopyScrollBox = Record & PixiScrollBox & ICopy;
+/* eslint-enable @typescript-eslint/no-explicit-any */
diff --git a/src/ct.release/templates.ts b/src/ct.release/templates.ts
index 704de0a41..2989c2697 100644
--- a/src/ct.release/templates.ts
+++ b/src/ct.release/templates.ts
@@ -5,17 +5,17 @@ import {Tilemap} from './tilemaps';
import roomsLib, {Room} from './rooms';
import {runBehaviors} from './behaviors';
import {copyTypeSymbol, stack} from '.';
-import stylesLib from 'styles';
import uLib from './u';
import type * as pixiMod from 'node_modules/pixi.js';
declare var PIXI: typeof pixiMod;
-import type {ExportedRoom, ExportedStyle, ExportedTemplate, TextureShape} from '../node_requires/exporter/_exporterContracts';
+import type {ExportedRoom, ExportedTemplate, TextureShape} from '../node_requires/exporter/_exporterContracts';
+import {CopyButton, CopyPanel, baseClassToPixiClass} from './templateBaseClasses';
let uid = 0;
-interface ICopy {
+export interface ICopy {
uid: number;
/** The name of the template from which the copy was created */
template: string | null;
@@ -139,62 +139,15 @@ export const setFocusedElement = (elt: IFocusableElement) => {
focusedElement = elt;
};
-import PixiButton from './templateBaseClasses/PixiButton';
-import PixiSpritedCounter from './templateBaseClasses/PixiSpritedCounter';
-import PixiScrollingTexture from './templateBaseClasses/PixiScrollingTexture';
-import PixiTextBox from './templateBaseClasses/PixiTextBox';
-
// Record allows ct.js users to write any properties to their copies
// without typescript complaining.
/* eslint-disable @typescript-eslint/no-explicit-any */
-
/**
* An instance of a ct.js template. Ct.js cannot predict the base class
* of a template here, so you may need to add `as Copy...` to further narrow down
* the class.
*/
export type BasicCopy = Record & pixiMod.DisplayObject & ICopy;
-/**
- * An instance of a ct.js template with Animated Sprite as its base class.
- * It has functionality of both PIXI.AnimatedSprite and ct.js Copies.
- */
-export type CopyAnimatedSprite = Record & pixiMod.AnimatedSprite & ICopy;
-/**
- * An instance of a ct.js template with Panel as its base class.
- * It has functionality of both PIXI.NineSlicePlane and ct.js Copies.
- */
-export type CopyPanel = Record & pixiMod.NineSlicePlane & ICopy;
-/**
- * An instance of a ct.js template with Text as its base class.
- * It has functionality of both PIXI.Text and ct.js Copies.
- */
-export type CopyText = Record & pixiMod.Text & 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
- * display anything, you can add other copies and pixi.js classes with `this.addChild(copy)`.
- */
-export type CopyContainer = Record & pixiMod.Container & ICopy;
-/**
- * An instance of a ct.js template with button logic.
- * It has functionality of both PIXI.Container and ct.js Copies.
- */
-export type CopyButton = Record & PixiButton & ICopy;
-/**
- * An instance of a ct.js template with text box logic.
- * It has functionality of both PIXI.Container and ct.js Copies.
- */
-export type CopyTextBox = Record & PixiTextBox & ICopy;
-/**
- * An instance of a ct.js template with repeating texture logic.
- * The texture can expand in any direction and can be animated by scrolling.
- */
-export type CopyRepeatingTexture = Record & PixiScrollingTexture & ICopy;
-/**
- * An instance of a ct.js template with a sprited counter logic.
- * This copy displays a number of identical sprites in a row, similar to sprited healthbars.
- */
-export type CopySpritedCounter = Record & PixiSpritedCounter & ICopy;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const CopyProto: Partial = {
@@ -324,18 +277,7 @@ const Copy = function (
this.baseClass = template.baseClass;
// Early linking so that `this.parent` is available in OnCreate events
this.parent = container;
- if (template.baseClass === 'AnimatedSprite') {
- this._tex = template.texture;
- const me = this as CopyAnimatedSprite;
- me.blendMode = template.blendMode || PIXI.BLEND_MODES.NORMAL;
- me.loop = template.loopAnimation;
- me.animationSpeed = template.animationFPS / 60;
- if (template.playAnimationOnStart) {
- me.play();
- }
- } else if (template.baseClass === 'NineSlicePlane') {
- const me = this as CopyPanel;
- me.blendMode = template.blendMode || PIXI.BLEND_MODES.NORMAL;
+ if (template.baseClass === 'AnimatedSprite' || template.baseClass === 'NineSlicePlane') {
this._tex = template.texture;
}
(this as Mutable).behaviors = [...template.behaviors];
@@ -441,107 +383,13 @@ export const makeCopy = (
throw new Error(`[ct.templates] An attempt to create a copy of a non-existent template \`${template}\` detected. A typo?`);
}
const t: ExportedTemplate = templatesLib.templates[template];
-
- // TODO: Refactor these into templateBaseClasses
- if (t.baseClass === 'Container') {
- const copy = new PIXI.Container() as CopyContainer;
- copy.shape = {
- type: 'point'
- };
- mix(copy, x, y, t, parent, exts);
- return copy;
- }
- if (t.baseClass === 'Text') {
- let style: ExportedStyle;
- if (t.textStyle && t.textStyle !== -1) {
- style = stylesLib.get(t.textStyle, true);
- }
- if (exts.customWordWrap) {
- style.wordWrap = true;
- style.wordWrapWidth = Number(exts.customWordWrap);
- }
- if (exts.customSize) {
- style.fontSize = Number(exts.customSize);
- }
- const copy = new PIXI.Text(
- (exts.customText as string) || t.defaultText || '',
- style as unknown as Partial
- ) as CopyText;
- if (exts.customAnchor) {
- const anchor = exts.customAnchor as {
- x?: number,
- y?: number
- };
- copy.anchor.set(anchor?.x ?? 0, anchor?.y ?? 0);
- }
- mix(copy, x, y, t, parent, exts);
- copy.shape = uLib.getRectShape(copy);
- copy.scale.set(
- (exts.scaleX as number) ?? 1,
- (exts.scaleY as number) ?? 1
- );
- return copy;
- }
-
- let textures: pixiMod.Texture[] = [PIXI.Texture.EMPTY];
- if (t.texture && t.texture !== '-1') {
- textures = resLib.getTexture(t.texture);
- }
-
- if (t.baseClass === 'NineSlicePlane') {
- const copy = new PIXI.NineSlicePlane(
- textures[0],
- t.nineSliceSettings?.left ?? 16,
- t.nineSliceSettings?.top ?? 16,
- t.nineSliceSettings?.right ?? 16,
- t.nineSliceSettings?.bottom ?? 16
- ) as CopyPanel;
- copy.updateNineSliceShape = t.nineSliceSettings.autoUpdate;
- mix(copy, x, y, t, parent, exts);
- const baseWidth = copy.width,
- baseHeight = copy.height;
- if ('scaleX' in exts) {
- copy.width = baseWidth * (exts.scaleX as number);
- }
- if ('scaleY' in exts) {
- copy.height = baseHeight * (exts.scaleY as number);
- }
- uLib.reshapeNinePatch(copy);
- return copy;
- }
- if (t.baseClass === 'Button') {
- const copy = new PixiButton(t, exts) as CopyButton;
- mix(copy, x, y, t, parent, exts);
- return copy;
- }
- if (t.baseClass === 'RepeatingTexture') {
- const copy = new PixiScrollingTexture(t, exts) as CopyRepeatingTexture;
- mix(copy, x, y, t, parent, exts);
- return copy;
- }
- if (t.baseClass === 'SpritedCounter') {
- const copy = new PixiSpritedCounter(t, exts) as CopySpritedCounter;
- mix(copy, x, y, t, parent, exts);
- return copy;
- }
- if (t.baseClass === 'AnimatedSprite') {
- const copy = new PIXI.AnimatedSprite(textures) as CopyAnimatedSprite;
- copy.anchor.x = t.anchorX ?? textures[0].defaultAnchor.x ?? 0;
- copy.anchor.y = t.anchorY ?? textures[0].defaultAnchor.y ?? 0;
- copy.scale.set(
- (exts.scaleX as number) ?? 1,
- (exts.scaleY as number) ?? 1
- );
- mix(copy, x, y, t, parent, exts);
- return copy;
- }
- if (t.baseClass === 'TextBox') {
- const copy = new PixiTextBox(t, exts) as CopyTextBox;
- mix(copy, x, y, t, parent, exts);
- return copy;
+ if (!(t.baseClass in baseClassToPixiClass)) {
+ throw new Error(`[internal -> makeCopy] Unknown base class \`${(t as any).baseClass}\` for template \`${template}\`.`);
}
+ const copy = new baseClassToPixiClass[t.baseClass](t, exts) as BasicCopy;
+ mix(copy, x, y, t, parent, exts);
+ return copy;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- throw new Error(`[internal -> makeCopy] Unknown base class \`${(t as any).baseClass}\` for template \`${template}\`.`);
};
const onCreateModifier = function () {
diff --git a/src/ct.release/u.ts b/src/ct.release/u.ts
index 56ceb1df2..6a675dab7 100644
--- a/src/ct.release/u.ts
+++ b/src/ct.release/u.ts
@@ -1,6 +1,7 @@
import type {CtjsTexture} from 'res';
import type {TextureShape} from '../node_requires/exporter/_exporterContracts';
-import type {BasicCopy, CopyButton, CopyPanel, CopyTextBox} from './templates';
+import type {BasicCopy} from './templates';
+import type {CopyButton, CopyPanel, CopyTextBox} from './templateBaseClasses';
import timerLib, {CtTimer} from './timer';
import {canvasCssPosition} from './fittoscreen';
import mainCamera from './camera';
diff --git a/src/node_requires/events/index.ts b/src/node_requires/events/index.ts
index db6595e25..6e36779b2 100644
--- a/src/node_requires/events/index.ts
+++ b/src/node_requires/events/index.ts
@@ -251,7 +251,7 @@ const getArgumentsTypeScript = (event: IEventDeclaration): string => {
};
import {baseClassToTS} from '../resources/templates';
-const baseTypes = `import {BasicCopy, ${Object.values(baseClassToTS).join(', ')}} from 'src/ct.release/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) {
diff --git a/src/node_requires/exporter/_exporterContracts.ts b/src/node_requires/exporter/_exporterContracts.ts
index c014c7e12..866f92a52 100644
--- a/src/node_requires/exporter/_exporterContracts.ts
+++ b/src/node_requires/exporter/_exporterContracts.ts
@@ -124,6 +124,7 @@ export type ExportedRoom = {
bindings: Record void>;
}
+export type BaseClass = TemplateBaseClass;
export type ExportedTemplate = {
name: string;
anchorX?: number;
@@ -175,7 +176,7 @@ export type ExportedTemplate = {
spriteCount: number;
texture: string;
} | {
- baseClass: 'TextBox',
+ baseClass: 'TextBox';
nineSliceSettings: ITemplate['nineSliceSettings'];
texture: string;
hoverTexture?: string;
@@ -186,6 +187,10 @@ export type ExportedTemplate = {
defaultText: string;
fieldType: ITemplate['fieldType'];
maxTextLength: number;
+} | {
+ baseClass: 'ScrollBox';
+ nineSliceSettings: ITemplate['nineSliceSettings'];
+ texture: string;
});
export type ExportedMeta = {
diff --git a/src/node_requires/hotkeys.js b/src/node_requires/hotkeys.js
index 52c21495d..c6a06446e 100644
--- a/src/node_requires/hotkeys.js
+++ b/src/node_requires/hotkeys.js
@@ -62,11 +62,20 @@ const getPriority = function getPriority(elt) {
return 0;
};
-const getCode = e => ''
+const getCode = e => {
+ let letter;
+ if (e.code.startsWith('Key')) {
+ letter = e.shiftKey ? e.code.slice(3) : e.code.slice(3).toLowerCase();
+ } else {
+ letter = e.key;
+ }
+ const code = ''
.concat(e.ctrlKey ? 'Control+' : '')
.concat(e.altKey ? 'Alt+' : '')
.concat(e.metaKey ? 'Meta+' : '')
- .concat(e.key);
+ .concat(letter);
+ return code;
+};
const listenerRef = Symbol('keydownListener');
const offDomEventsRef = Symbol('offDomEventsRef');
diff --git a/src/node_requires/resources/emitterTandems/index.ts b/src/node_requires/resources/emitterTandems/index.ts
index 644785e8e..5b7d69dda 100644
--- a/src/node_requires/resources/emitterTandems/index.ts
+++ b/src/node_requires/resources/emitterTandems/index.ts
@@ -1,4 +1,7 @@
+import generateGUID from '../../generateGUID';
+import {IAssetContextItem, addAsset} from '..';
import {promptName} from '../promptName';
+import {getByPath} from '../../i18n';
const getThumbnail = function getThumbnail(): string {
return 'sparkles';
@@ -6,32 +9,98 @@ const getThumbnail = function getThumbnail(): string {
import * as defaultEmitter from './defaultEmitter';
-const createNewTandem = async (): Promise => {
- const name = await promptName('tandem', 'New Emitter Tandem');
- if (!name) {
- // eslint-disable-next-line no-throw-literal
- throw 'cancelled';
- }
+const YAML = require('js-yaml');
+import {writeFile, readFile} from 'fs-extra';
+
+const createNewTandem = async (opts: {src?: string}): Promise => {
+ if (!opts || !('src' in opts)) {
+ const name = await promptName('tandem', 'New Emitter Tandem');
+ if (!name) {
+ // eslint-disable-next-line no-throw-literal
+ throw 'cancelled';
+ }
+
+ const emitter = defaultEmitter.get();
+ const id = generateGUID();
- const emitter = defaultEmitter.get();
- const generateGUID = require('./../../generateGUID');
+ const tandem: ITandem = {
+ name,
+ uid: id,
+ emitters: [emitter],
+ lastmod: Number(new Date()),
+ type: 'tandem'
+ };
+
+ return tandem;
+ }
+ // Importing from file
+ const source = YAML.load(await readFile(opts.src)) as Partial;
+ const keys: (keyof ITandem)[] = [
+ 'name',
+ 'type',
+ 'emitters'
+ ];
+ // Check for missing fields — a user might select a wrong file
+ for (const key of keys) {
+ if (!(key in source)) {
+ const message = getByPath('createAsset.formatError') as string;
+ alertify.error(message);
+ throw new Error(message);
+ }
+ }
const id = generateGUID(),
slice = id.slice(-6);
-
const tandem = {
- name,
+ name: 'Unnamed Tandem',
uid: id,
origname: 'pt' + slice,
- emitters: [emitter],
+ emitters: [],
lastmod: Number(new Date()),
type: 'tandem'
} as ITandem;
-
+ Object.assign(tandem, source);
+ for (const emitter of tandem.emitters) {
+ emitter.uid = generateGUID();
+ }
return tandem;
};
export const areThumbnailsIcons = true;
+export const assetContextMenuItems: IAssetContextItem[] = [{
+ icon: 'copy',
+ vocPath: 'common.duplicate',
+ action: (asset: ITandem, collection, folder): void => {
+ const newTandem = structuredClone(asset) as ITandem & {uid: string};
+ newTandem.uid = generateGUID();
+ newTandem.name += `_${newTandem.uid.slice(0, 4)}`;
+ addAsset(newTandem, folder);
+ }
+}, {
+ icon: 'upload',
+ action: async (asset: ITandem): Promise => {
+ const savePath = await window.showSaveDialog({
+ defaultName: `${asset.name}.ctTandem`,
+ filter: '.ctTandem'
+ });
+ if (!savePath) {
+ return;
+ }
+ const copy = {
+ ...asset
+ };
+ delete copy.uid;
+ delete copy.lastmod;
+ for (const emitter of copy.emitters) {
+ delete emitter.uid;
+ emitter.texture = -1;
+ }
+ await writeFile(savePath, YAML.dump(copy));
+ alertify.success(getByPath('common.done'));
+ },
+ vocPath: 'assetViewer.exportTandem'
+}];
+
export {
getThumbnail,
defaultEmitter,
diff --git a/src/node_requires/resources/rooms/IRoom.d.ts b/src/node_requires/resources/rooms/IRoom.d.ts
index 8962fadb8..e054dba0d 100644
--- a/src/node_requires/resources/rooms/IRoom.d.ts
+++ b/src/node_requires/resources/rooms/IRoom.d.ts
@@ -108,6 +108,7 @@ interface IRoom extends IScriptableBehaviors {
gridX: number;
gridY: number;
diagonalGrid: boolean;
+ disableGrid: boolean;
simulate: boolean;
restrictCamera?: boolean;
restrictMinX?: number;
diff --git a/src/node_requires/resources/rooms/defaultRoom.ts b/src/node_requires/resources/rooms/defaultRoom.ts
index f4534a22a..63f1ba5eb 100644
--- a/src/node_requires/resources/rooms/defaultRoom.ts
+++ b/src/node_requires/resources/rooms/defaultRoom.ts
@@ -9,6 +9,7 @@ const room = {
gridX: 64,
gridY: 64,
diagonalGrid: false,
+ disableGrid: false,
simulate: true,
width: 1280,
height: 720,
diff --git a/src/node_requires/resources/sounds/index.ts b/src/node_requires/resources/sounds/index.ts
index ada6f1c1a..2e839eb36 100644
--- a/src/node_requires/resources/sounds/index.ts
+++ b/src/node_requires/resources/sounds/index.ts
@@ -1,19 +1,27 @@
-import {SoundPreviewer} from '../preview/sound';
+import path from 'path';
+import fs from 'fs-extra';
-const path = require('path'),
- fs = require('fs-extra');
+import {SoundPreviewer} from '../preview/sound';
+import {promptName} from '../promptName';
import {sound} from 'node_modules/@pixi/sound';
export const getThumbnail = SoundPreviewer.getClassic;
export const areThumbnailsIcons = false;
-export const createAsset = function (): ISound {
+export const createAsset = async (name?: string): Promise => {
+ if (!name) {
+ const newName = await promptName('sound', 'New Sound');
+ if (!newName) {
+ // eslint-disable-next-line no-throw-literal
+ throw 'cancelled';
+ }
+ name = newName;
+ }
const generateGUID = require('./../../generateGUID');
- var id = generateGUID(),
- slice = id.slice(-6);
+ var id = generateGUID();
const newSound: ISound = {
- name: ('Sound_' + slice),
+ name,
uid: id,
type: 'sound' as const,
lastmod: Number(new Date()),
diff --git a/src/node_requires/riotMixins/discardio.ts b/src/node_requires/riotMixins/discardio.ts
index 9f1d8dbcc..e5495677b 100644
--- a/src/node_requires/riotMixins/discardio.ts
+++ b/src/node_requires/riotMixins/discardio.ts
@@ -28,7 +28,10 @@ const discardio = (riotTag: IRiotTag) => {
riotTag.asset.lastmod = Number(new Date());
const sourceObject = discardioSources.get(riotTag);
const changedObject = riotTag.asset;
- // update the innards of the object without replacing it completely
+ // update the innards of the object without creating a new one
+ for (const key of Object.keys(sourceObject)) {
+ delete sourceObject[key as keyof typeof sourceObject];
+ }
Object.assign(sourceObject, changedObject);
window.signals.trigger('assetChanged', riotTag.asset.uid);
window.signals.trigger(`${riotTag.asset.type}Changed`, riotTag.asset.uid);
diff --git a/src/node_requires/roomEditor/entityClasses/Copy.ts b/src/node_requires/roomEditor/entityClasses/Copy.ts
index ee549d466..578219586 100644
--- a/src/node_requires/roomEditor/entityClasses/Copy.ts
+++ b/src/node_requires/roomEditor/entityClasses/Copy.ts
@@ -155,8 +155,8 @@ class Copy extends PIXI.Container {
}
get tint(): PIXI.ColorSource {
return this.sprite?.tint ||
- this.text?.tint ||
this.nineSlicePlane?.tint ||
+ this.text?.tint ||
this.tilingSprite?.tint ||
this.#tint;
}
diff --git a/src/node_requires/roomEditor/index.ts b/src/node_requires/roomEditor/index.ts
index d09a1064c..12c352df9 100644
--- a/src/node_requires/roomEditor/index.ts
+++ b/src/node_requires/roomEditor/index.ts
@@ -657,6 +657,7 @@ class RoomEditor extends PIXI.Application {
beforeTileLayers,
afterTileLayers
});
+ this.transformer.setup();
}
drawSelection(entities: Iterable): void {
this.selectionOverlay.clear();
diff --git a/src/node_requires/themes/index.ts b/src/node_requires/themes/index.ts
index e19dee5ac..e12a9b065 100644
--- a/src/node_requires/themes/index.ts
+++ b/src/node_requires/themes/index.ts
@@ -5,6 +5,10 @@ import {getLanguageJSON} from '../i18n';
const defaultTheme = 'Day';
const defaultMonacoTheme = defaultTheme;
+/**
+ * The list of the built-in themes coupled with the list of accent colors
+ * shown in the theme list.
+ */
const builtInThemes: [string, string[]][] = [
['Day', ['#ffffff', '#5144db', '#446adb']],
['SpringStream', ['#ffffff', '#00c09e']],
diff --git a/src/riotTags/app-view.tag b/src/riotTags/app-view.tag
index 0498c4da6..3b75de847 100644
--- a/src/riotTags/app-view.tag
+++ b/src/riotTags/app-view.tag
@@ -159,7 +159,7 @@ app-view.flexcol
window.hotkeys.push(tab);
} else {
// The current tab is an asset
- if (['room', 'template', 'behavior'].includes(tab.type)) {
+ if (['room', 'template', 'behavior', 'script'].includes(tab.type)) {
window.orders.trigger('forceCodeEditorLayout');
}
window.hotkeys.push(tab.uid);
diff --git a/src/riotTags/editors/emitter-tandem-editor/emitter-editor.tag b/src/riotTags/editors/emitter-tandem-editor/emitter-editor.tag
index 0927eb00e..7bcfd2f2a 100644
--- a/src/riotTags/editors/emitter-tandem-editor/emitter-editor.tag
+++ b/src/riotTags/editors/emitter-tandem-editor/emitter-editor.tag
@@ -495,7 +495,12 @@ emitter-editor.aPanel.pad.nb
this.updateShortcuts();
const {getThumbnail, getById} = require('./data/node_requires/resources');
- this.getPreview = () => getThumbnail(getById('texture', this.opts.emitter.texture));
+ this.getPreview = () => {
+ if (this.opts.emitter.texture === -1) {
+ return '/data/img/unknown.png';
+ }
+ return getThumbnail(getById('texture', this.opts.emitter.texture));
+ };
this.wireAndReset = path => e => {
this.wire(path)(e);
diff --git a/src/riotTags/editors/room-editor/room-editor.tag b/src/riotTags/editors/room-editor/room-editor.tag
index d39243d79..5a044633d 100644
--- a/src/riotTags/editors/room-editor/room-editor.tag
+++ b/src/riotTags/editors/room-editor/room-editor.tag
@@ -3,7 +3,7 @@
The room to edit
@attribute ondone (riot function)
-room-editor.aPanel.aView
+room-editor.aPanel.aView(data-hotkey-scope="{asset.uid}")
canvas(ref="canvas" oncontextmenu="{openMenus}")
// Toolbar
.room-editor-aToolsetHolder
@@ -13,7 +13,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'select'}"
title="{voc.tools.select} (Q)"
data-hotkey="q"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#cursor")
@@ -22,7 +22,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'addCopies'}"
title="{voc.tools.addCopies} (W)"
data-hotkey="w"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#template")
@@ -31,7 +31,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'addTiles'}"
title="{voc.tools.addTiles} (E)"
data-hotkey="e"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#grid")
@@ -40,7 +40,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'manageBackgrounds'}"
title="{voc.tools.manageBackgrounds} (R)"
data-hotkey="r"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#image")
@@ -49,7 +49,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'uiTools'}"
title="{voc.tools.uiTools} (T)"
data-hotkey="t"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#ui")
@@ -58,7 +58,7 @@ room-editor.aPanel.aView
class="{active: currentTool === 'roomProperties'}"
title="{voc.tools.roomProperties} (Y)"
data-hotkey="y"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
svg.feather
use(xlink:href="#settings")
@@ -132,7 +132,7 @@ room-editor.aPanel.aView
onchange="{changeSimulated}"
checked="{pixiEditor?.simulate}"
data-hotkey="S"
- data-hotkey-require-scope="rooms"
+ data-hotkey-require-scope="{asset.uid}"
)
span {voc.simulate}
button(onclick="{openZoomMenu}")
@@ -209,13 +209,31 @@ room-editor.aPanel.aView
return;
}
this.gridOn = !this.gridOn;
+ this.asset.disableGrid = !this.gridOn;
+ };
+ const checkRefs = deleted => {
+ let cleaned = false;
+ if (this.asset.follow === deleted) {
+ this.asset.follow = -1;
+ cleaned = true;
+ console.log(`Removed a template with ID ${deleted} from a room ${this.asset.name}.`);
+ }
+ if (this.asset.behaviors.find(b => b === deleted)) {
+ this.asset.behaviors = this.asset.behaviors.filter(b => b !== deleted);
+ cleaned = true;
+ console.log(`Removed a behavior with ID ${deleted} from a room ${this.asset.name}.`);
+ }
+ if (cleaned) {
+ this.update();
+ }
};
this.on('mount', () => {
- window.hotkeys.push('roomEditor');
window.hotkeys.on('Control+g', gridToggleListener);
document.addEventListener('keydown', modifiersDownListener);
document.addEventListener('keyup', modifiersUpListener);
window.addEventListener('blur', blurListener);
+ window.signals.on('templateRemoved', checkRefs);
+ window.signals.on('behaviorRemoved', checkRefs);
});
this.on('unmount', () => {
window.hotkeys.exit('roomEditor');
@@ -223,6 +241,8 @@ room-editor.aPanel.aView
document.removeEventListener('keydown', modifiersDownListener);
document.removeEventListener('keyup', modifiersUpListener);
window.addEventListener('blur', blurListener);
+ window.signals.off('templateRemoved', checkRefs);
+ window.signals.off('behaviorRemoved', checkRefs);
});
this.lockableTypes = [{
@@ -371,13 +391,14 @@ room-editor.aPanel.aView
this.pixiEditor.simulate = !this.pixiEditor.simulate;
};
- this.gridOn = true;
+ this.gridOn = !this.asset.disableGrid;
this.gridMenu = {
opened: false,
items: [{
label: this.voc.gridOff,
click: () => {
this.gridOn = !this.gridOn;
+ this.asset.disableGrid = !this.gridOn;
},
type: 'checkbox',
checked: () => !this.gridOn,
diff --git a/src/riotTags/editors/style-editor.tag b/src/riotTags/editors/style-editor.tag
index 47276e210..a698ee1e3 100644
--- a/src/riotTags/editors/style-editor.tag
+++ b/src/riotTags/editors/style-editor.tag
@@ -20,7 +20,7 @@ style-editor.aPanel.aView(class="{opts.class}")
label.fifty.npl.nmt
b {voc.fontSize}
br
- input#fontsize.wide(type="number" value="{asset.font.size || '12'}" onchange="{wire('asset.font.size')}" oninput="{wire('asset.font.size')}" step="1")
+ input#fontsize.wide(type="number" value="{asset.font.size || '12'}" onchange="{wireFontSize}" oninput="{wireFontSize}" step="1")
label.fifty.npr.nmt
b {voc.fontWeight}
br
@@ -136,13 +136,22 @@ style-editor.aPanel.aView(class="{opts.class}")
svg.feather
use(xlink:href="#check")
span {vocGlob.apply}
- .style-editor-aPreview.tall(ref="canvasSlot")
+ .style-editor-aPreview.tall(ref="canvasSlot" style="background-color: {previewColor};")
+ button.inline.forcebackground.style-editor-aChangeBgButton(onclick="{changePreviewBg}")
+ svg.feather
+ use(xlink:href="#droplet")
+ span {vocFull.textureView.bgColor}
asset-selector(
if="{selectingFont}"
assettypes="font"
onselected="{applyFont}"
oncancelled="{cancelCustomFontSelector}"
)
+ color-picker(
+ ref="previewBackgroundColor" if="{changingPreviewBg}"
+ hidealpha="true"
+ color="{previewColor}" onapply="{updatePreviewColor}" onchanged="{updatePreviewColor}" oncancel="{cancelPreviewColor}"
+ )
script.
this.namespace = 'styleView';
this.mixin(require('./data/node_requires/riotMixins/voc').default);
@@ -157,12 +166,13 @@ style-editor.aPanel.aView(class="{opts.class}")
this.tab = tab;
};
this.on('mount', () => {
- const width = 800;
- const height = 500;
+ const bounds = this.refs.canvasSlot.getBoundingClientRect();
+ const width = Math.floor(bounds.width);
+ const height = Math.floor(bounds.height);
this.pixiApp = new PIXI.Application({
width,
height,
- transparent: true
+ backgroundAlpha: 0
});
this.refs.canvasSlot.appendChild(this.pixiApp.view);
@@ -195,6 +205,17 @@ style-editor.aPanel.aView(class="{opts.class}")
this.on('updated', () => {
this.refreshStyleTexture();
});
+ const resizeCanvas = () => {
+ const bounds = this.refs.canvasSlot.getBoundingClientRect();
+ this.pixiApp.renderer.resize(Math.floor(bounds.width), Math.floor(bounds.height));
+ for (const label of this.labels) {
+ label.x = bounds.width / 2;
+ }
+ };
+ window.addEventListener('resize', resizeCanvas);
+ this.on('unmount', () => {
+ window.removeEventListener('resize', resizeCanvas);
+ });
this.selectingTexture = false;
@@ -220,6 +241,14 @@ style-editor.aPanel.aView(class="{opts.class}")
alertify.success(this.vocGlob.done);
};
+ this.wireFontSize = e => {
+ const oldSize = this.asset.font.size,
+ oldLineHeight = this.asset.font.lineHeight;
+ this.wire('asset.font.size')(e);
+ const k = this.asset.font.size / oldSize;
+ this.asset.font.lineHeight = Math.round(oldLineHeight * k * 100) / 100;
+ };
+
this.styleSetAlign = align => () => {
this.asset.font.halign = align;
};
@@ -275,3 +304,27 @@ style-editor.aPanel.aView(class="{opts.class}")
await this.saveAsset();
this.opts.ondone(this.asset);
};
+
+ // Color of the preview window and changing it
+ const themesAPI = require('./data/node_requires/themes');
+ console.log(themesAPI);
+ const {getSwatch} = require('./data/node_requires/themes');
+ this.previewColor = getSwatch('backgroundDeeper');
+ this.changePreviewBg = () => {
+ this.changingPreviewBg = !this.changingPreviewBg;
+ if (this.changingPreviewBg) {
+ this.oldPreviewColor = this.previewColor;
+ }
+ };
+ this.updatePreviewColor = (color, evtype) => {
+ this.previewColor = color;
+ if (evtype === 'onapply') {
+ this.changingPreviewBg = false;
+ }
+ this.update();
+ };
+ this.cancelPreviewColor = () => {
+ this.changingPreviewBg = false;
+ this.previewColor = this.oldPreviewColor;
+ this.update();
+ };
diff --git a/src/riotTags/editors/template-editor.tag b/src/riotTags/editors/template-editor.tag
index 10d354883..dc7238cac 100644
--- a/src/riotTags/editors/template-editor.tag
+++ b/src/riotTags/editors/template-editor.tag
@@ -450,15 +450,21 @@ template-editor.aPanel.aView.flexrow
cleaned = true;
}
}
+ if (this.asset.behaviors.find(b => b === deleted)) {
+ this.asset.behaviors = this.asset.behaviors.filter(b => b !== deleted);
+ cleaned = true;
+ }
if (cleaned) {
this.update();
}
};
window.signals.on('textureRemoved', checkRefs);
window.signals.on('styleRemoved', checkRefs);
+ window.signals.on('behaviorRemoved', checkRefs);
this.on('unmount', () => {
window.signals.off('textureRemoved', checkRefs);
window.signals.off('styleRemoved', checkRefs);
+ window.signals.off('behaviorRemoved', checkRefs);
});
this.minimizeProps = localStorage.minimizeTemplatesProps === 'yes';
diff --git a/src/riotTags/project-selector.tag b/src/riotTags/project-selector.tag
index 9654e15b8..c9e9bf6ad 100644
--- a/src/riotTags/project-selector.tag
+++ b/src/riotTags/project-selector.tag
@@ -414,28 +414,49 @@ project-selector
};
// Checking for updates
- setTimeout(() => {
- const {isWin, isLinux} = require('./data/node_requires/platformUtils.js');
- let channel = 'osx64';
- if (isWin) {
- channel = 'win64';
- } else if (isLinux) {
- channel = 'linux64';
+ // Cache update status for an hour to not DDoS itch.io while developing.
+ let needsUpdateCheck = false,
+ lastUpdateCheck;
+ if (localStorage.lastUpdateCheck) {
+ lastUpdateCheck = new Date(localStorage.lastUpdateCheck);
+ // Check once an hour
+ if ((new Date()) - lastUpdateCheck > 1000 * 60 * 60) {
+ needsUpdateCheck = true;
}
- fetch(`https://itch.io/api/1/x/wharf/latest?target=comigo/ct&channel_name=${channel}`)
- .then(response => response.json())
- .then(json => {
- if (!json.errors) {
- if (this.ctjsVersion !== json.latest) {
- this.newVersion = this.voc.latestVersion.replace('$1', json.latest);
- this.update();
- }
- } else {
- console.error('Update check failed:');
- console.error(json.errors);
+ } else {
+ needsUpdateCheck = true;
+ }
+ if (needsUpdateCheck) {
+ setTimeout(() => {
+ const {isWin, isLinux} = require('./data/node_requires/platformUtils.js');
+ let channel = 'osx64';
+ if (isWin) {
+ channel = 'win64';
+ } else if (isLinux) {
+ channel = 'linux64';
}
- });
- }, 0);
+ fetch(`https://itch.io/api/1/x/wharf/latest?target=comigo/ct&channel_name=${channel}`)
+ .then(response => response.json())
+ .then(json => {
+ if (!json.errors) {
+ if (this.ctjsVersion !== json.latest) {
+ this.newVersion = this.voc.latestVersion.replace('$1', json.latest);
+ this.update();
+ }
+ localStorage.lastUpdateCheck = new Date();
+ localStorage.lastUpdateCheckVersion = json.latest;
+ } else {
+ console.error('Update check failed:');
+ console.error(json.errors);
+ }
+ });
+ }, 0);
+ } else {
+ const newVersion = localStorage.lastUpdateCheckVersion;
+ if (this.ctjsVersion !== newVersion) {
+ this.newVersion = this.voc.latestVersion.replace('$1', newVersion);
+ }
+ }
this.openExternal = link => e => {
nw.Shell.openExternal(link);
diff --git a/src/riotTags/shared/asset-browser.tag b/src/riotTags/shared/asset-browser.tag
index 109295961..e9ff1bb9c 100644
--- a/src/riotTags/shared/asset-browser.tag
+++ b/src/riotTags/shared/asset-browser.tag
@@ -170,6 +170,8 @@ asset-browser.flexfix(class="{opts.namespace} {opts.class} {compact: opts.compac
span.secondary(if="{asset.type !== 'folder' && (parent.assetTypes.length > 1 || parent.assetTypes[0] === 'all')}")
svg.feather
use(xlink:href="#{iconMap[asset.type]}")
+ svg.feather(if="{asset.type === 'behavior'}")
+ use(xlink:href="#{iconMap[asset.behaviorType]}")
span(if="{!parent.opts.compact}") {vocGlob.assetTypes[asset.type][0].slice(0, 1).toUpperCase()}{vocGlob.assetTypes[asset.type][0].slice(1)}
.asset-browser-Icons(if="{asset.type !== 'folder'}")
svg.feather(each="{icon in parent.getIcons(asset)}" class="feather-{icon}")
diff --git a/src/riotTags/shared/create-asset-menu.tag b/src/riotTags/shared/create-asset-menu.tag
index f0c324bdc..ae84dd763 100644
--- a/src/riotTags/shared/create-asset-menu.tag
+++ b/src/riotTags/shared/create-asset-menu.tag
@@ -34,7 +34,7 @@ create-asset-menu.relative.inlineblock(class="{opts.class}")
this.mixin(require('./data/node_requires/riotMixins/voc').default);
const priorityTypes = ['texture', 'template', 'room'];
- const customizedTypes = ['behavior'];
+ const customizedTypes = ['tandem', 'behavior'];
const {assetTypes, resourceToIconMap, createAsset} = require('./data/node_requires/resources');
@@ -47,29 +47,28 @@ create-asset-menu.relative.inlineblock(class="{opts.class}")
this.update();
};
+ const genericCreate = (assetType, payload) => async () => {
+ try {
+ const asset = await createAsset(assetType, this.opts.folder || null, payload);
+ if (asset === null) {
+ return; // Cancelled by a user
+ }
+ if (this.opts.onimported) {
+ this.opts.onimported(asset);
+ }
+ } catch (e) {
+ alertify.error(e);
+ throw e;
+ }
+ };
+
const menuItems = [];
const assetTypeIterator = assetType => {
const [i18nName] = this.vocGlob.assetTypes[assetType];
menuItems.push({
label: i18nName[0].toUpperCase() + i18nName.slice(1),
icon: resourceToIconMap[assetType],
- click: async () => {
- try {
- const asset = await createAsset(
- assetType,
- this.opts.folder || null
- );
- if (asset === null) {
- return; // Cancelled by a user
- }
- if (this.opts.onimported) {
- this.opts.onimported(asset);
- }
- } catch (e) {
- alertify.error(e);
- throw e;
- }
- }
+ click: genericCreate(assetType)
});
};
priorityTypes.forEach(assetTypeIterator);
@@ -81,42 +80,58 @@ create-asset-menu.relative.inlineblock(class="{opts.class}")
!customizedTypes.includes(assetType))
.forEach(assetTypeIterator);
- // Behaviors need a subtype preset
- const bhVoc = this.vocGlob.assetTypes.behavior;
+ // Tandems can be imported
+ const tandemVoc = this.vocGlob.assetTypes.tandem;
menuItems.push({
- label: bhVoc[1].slice(0, 1).toUpperCase() + bhVoc[1].slice(1),
- icon: 'behavior',
+ label: tandemVoc[0].slice(0, 1).toUpperCase() + tandemVoc[0].slice(1),
+ icon: 'sparkles',
+ click: genericCreate('tandem'),
submenu: {
items: [{
- label: this.voc.behaviorTemplate,
- icon: 'behavior',
+ label: this.vocGlob.create,
+ icon: 'plus',
+ click: genericCreate('tandem')
+ }, {
+ label: this.voc.importFromFile,
+ icon: 'download',
click: async () => {
- const asset = await createAsset('behavior', this.opts.folder || null, {
- behaviorType: 'template'
+ const src = await window.showOpenDialog({
+ filter: '.ctTandem'
});
- if (asset === null) {
- return; // Cancelled by a user
+ if (!src) {
+ return;
}
+ const asset = await createAsset('tandem', this.opts.folder || null, {
+ src
+ });
if (this.opts.onimported) {
this.opts.onimported(asset);
}
}
+ }]
+ }
+ });
+
+ // Behaviors need a subtype preset and can be imported
+ const bhVoc = this.vocGlob.assetTypes.behavior;
+ menuItems.push({
+ label: bhVoc[0].slice(0, 1).toUpperCase() + bhVoc[0].slice(1),
+ icon: 'behavior',
+ submenu: {
+ items: [{
+ label: this.voc.behaviorTemplate,
+ icon: 'template',
+ click: genericCreate('behavior', {
+ behaviorType: 'template'
+ })
}, {
label: this.voc.behaviorRoom,
- icon: 'behavior',
- click: async () => {
- const asset = await createAsset('behavior', this.opts.folder || null, {
- behaviorType: 'room'
- });
- if (asset === null) {
- return; // Cancelled by a user
- }
- if (this.opts.onimported) {
- this.opts.onimported(asset);
- }
- }
+ icon: 'room',
+ click: genericCreate('behavior', {
+ behaviorType: 'room'
+ })
}, {
- label: this.voc.behaviorImport,
+ label: this.voc.importFromFile,
icon: 'download',
click: async () => {
const src = await window.showOpenDialog({
diff --git a/src/styl/tags/editors/style-editor.styl b/src/styl/tags/editors/style-editor.styl
index 68f423936..17cef8f9d 100644
--- a/src/styl/tags/editors/style-editor.styl
+++ b/src/styl/tags/editors/style-editor.styl
@@ -17,3 +17,12 @@ style-editor, [data-is="style-editor"]
margin-bottom 0.5rem
.flexfix-footer
margin-top 0.5rem
+ & > button
+ margin-top 0.5rem
+.style-editor-aPreview
+ overflow hidden
+ position relative
+.style-editor-aChangeBgButton
+ position absolute
+ left 0.5rem
+ bottom 0.5rem