From ed0d0b30bbbd6046c51d4923959e39a6900c7e50 Mon Sep 17 00:00:00 2001 From: Cosmo Myzrail Gorynych Date: Thu, 30 Jul 2020 13:44:24 +1200 Subject: [PATCH] :sparkles: module that allows you to call parents' code and keep things DRY\nAlso unifies input fields at module settings and asset extensions --- OurRiotDocFormat.md | 2 +- app/data/ct.libs/.eslintrc.json | 7 +- app/data/ct.libs/inherit/InheritanceTest.ict | 369 ++++++++++++++++++ .../i15293c51-6bdc-4a62-941e-0b08656796a7.png | Bin 0 -> 2475 bytes ...1-6bdc-4a62-941e-0b08656796a7.png_prev.png | Bin 0 -> 2201 bytes ...6bdc-4a62-941e-0b08656796a7.png_prev@2.png | Bin 0 -> 4286 bytes .../i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png | Bin 0 -> 394 bytes ...a-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png | Bin 0 -> 948 bytes ...1c6d-4020-8aa5-1268faa5cbeb.png_prev@2.png | Bin 0 -> 1412 bytes .../i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png | Bin 0 -> 2372 bytes ...1-1d13-4f67-ac3d-cccec7b01cc4.png_prev.png | Bin 0 -> 2374 bytes ...1d13-4f67-ac3d-cccec7b01cc4.png_prev@2.png | Bin 0 -> 4818 bytes .../InheritanceTest/img/r428ea16b4401.png | Bin 0 -> 7715 bytes .../InheritanceTest/img/re8d64a493cce.png | Bin 0 -> 7859 bytes .../inherit/InheritanceTest/img/splash.png | Bin 0 -> 7859 bytes app/data/ct.libs/inherit/README.md | 38 ++ app/data/ct.libs/inherit/index.js | 50 +++ .../ct.libs/inherit/injects/onbeforecreate.js | 40 ++ app/data/ct.libs/inherit/module.json | 15 + app/data/ct.libs/inherit/types.d.ts | 75 ++++ app/data/ct.libs/place/module.json | 10 +- app/data/i18n/English.json | 3 +- app/data/i18n/Russian.json | 1 + app/package-lock.json | 6 +- package-lock.json | 15 +- src/node_requires/exporter/rooms.js | 3 +- src/node_requires/exporter/types.js | 7 +- src/node_requires/exporter/utils.js | 51 +++ .../resources/types/defaultType.js | 1 + src/node_requires/resources/types/index.js | 35 +- src/riotTags/modules-panel.tag | 43 +- .../project-settings/project-settings.tag | 2 +- src/riotTags/rooms/room-tile-editor.tag | 2 +- src/riotTags/shared/asset-viewer.tag | 6 +- src/riotTags/shared/extensions-editor.tag | 128 ++++-- src/riotTags/shared/type-input.tag | 66 ++++ src/riotTags/shared/type-selector.tag | 26 ++ src/riotTags/type-editor.tag | 2 +- src/riotTags/types-panel.tag | 4 +- src/styl/tags/shared/extensions-editor.styl | 5 + src/styl/tags/shared/texture-selector.styl | 4 + src/styl/tags/shared/type-input.styl | 5 + src/styl/tags/shared/type-selector.styl | 2 + src/styl/tags/textures/texture-selector.styl | 2 - src/styl/tags/textures/textures-panel.styl | 7 +- 45 files changed, 930 insertions(+), 102 deletions(-) create mode 100644 app/data/ct.libs/inherit/InheritanceTest.ict create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png_prev.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png_prev@2.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/r428ea16b4401.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png create mode 100644 app/data/ct.libs/inherit/InheritanceTest/img/splash.png create mode 100644 app/data/ct.libs/inherit/README.md create mode 100644 app/data/ct.libs/inherit/index.js create mode 100644 app/data/ct.libs/inherit/injects/onbeforecreate.js create mode 100644 app/data/ct.libs/inherit/module.json create mode 100644 app/data/ct.libs/inherit/types.d.ts create mode 100644 src/node_requires/exporter/utils.js create mode 100644 src/riotTags/shared/type-input.tag create mode 100644 src/riotTags/shared/type-selector.tag create mode 100644 src/styl/tags/shared/extensions-editor.styl create mode 100644 src/styl/tags/shared/texture-selector.styl create mode 100644 src/styl/tags/shared/type-input.styl create mode 100644 src/styl/tags/shared/type-selector.styl delete mode 100644 src/styl/tags/textures/texture-selector.styl diff --git a/OurRiotDocFormat.md b/OurRiotDocFormat.md index cd0f766aa..913ac42f7 100644 --- a/OurRiotDocFormat.md +++ b/OurRiotDocFormat.md @@ -25,7 +25,7 @@ At `./src/riotTags`, use this syntax at the top of the file to document tags: A description of a tag's method that can be safely called for inter-module communications. @property propertyName (type, typeSpecificator) - A descriotion of an exposed property of a tag. + A description of an exposed property of a tag. ``` Optional attributes are inside square brackets. diff --git a/app/data/ct.libs/.eslintrc.json b/app/data/ct.libs/.eslintrc.json index d64caeed7..b2d14ecc8 100644 --- a/app/data/ct.libs/.eslintrc.json +++ b/app/data/ct.libs/.eslintrc.json @@ -1,7 +1,12 @@ { "globals": { "ct": false, - "PIXI": true + "PIXI": false, + "Room": false, + "Background": false, + "Tileset": false, + "Copy": false, + "Camera": false }, "rules": { "object-shorthand": 0, diff --git a/app/data/ct.libs/inherit/InheritanceTest.ict b/app/data/ct.libs/inherit/InheritanceTest.ict new file mode 100644 index 000000000..a4f81c666 --- /dev/null +++ b/app/data/ct.libs/inherit/InheritanceTest.ict @@ -0,0 +1,369 @@ +ctjsVersion: 1.3.2 +notes: /* empty */ +libs: + place: + gridX: 1024 + gridY: 1024 + fittoscreen: + mode: scaleFit + mouse: {} + keyboard: {} + keyboard.polyfill: {} + sound.howler: {} + assert: {} + inherit: {} +textures: + - name: robot_greenDrive1 + untill: 0 + grid: + - 1 + - 1 + axis: + - 79 + - 60 + marginx: 0 + marginy: 0 + imgWidth: 158 + imgHeight: 120 + width: 158 + height: 120 + offx: 0 + offy: 0 + origname: i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png + source: >- + /home/comigo/Downloads/kenney)robot-pack/PNG/Side + view/robot_greenDrive1.png + shape: rect + left: 79 + right: 79 + top: 60 + bottom: 60 + uid: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + padding: 1 + lastmod: 1595996924807 + - name: robot_redDrive2 + untill: 0 + grid: + - 1 + - 1 + axis: + - 90 + - 73 + marginx: 0 + marginy: 0 + imgWidth: 180 + imgHeight: 146 + width: 180 + height: 146 + offx: 0 + offy: 0 + origname: i15293c51-6bdc-4a62-941e-0b08656796a7.png + source: /home/comigo/Downloads/kenney)robot-pack/PNG/Side view/robot_redDrive2.png + shape: rect + left: 90 + right: 90 + top: 73 + bottom: 73 + uid: 15293c51-6bdc-4a62-941e-0b08656796a7 + padding: 1 + lastmod: 1595996927142 + - name: magicMid + untill: 0 + grid: + - 1 + - 1 + axis: + - 0 + - 0 + marginx: 0 + marginy: 0 + imgWidth: 70 + imgHeight: 105 + width: 70 + height: 105 + offx: 0 + offy: 0 + origname: i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png + source: /home/comigo/Downloads/kenney_platformerbricks_updated/PNG/magicMid.png + shape: rect + left: 0 + right: 70 + top: 0 + bottom: 105 + uid: 387786ba-1c6d-4020-8aa5-1268faa5cbeb + padding: 1 +skeletons: [] +types: + - name: AbstractMonster + depth: 0 + oncreate: |- + this.speed = 10; + this.direction = 180; + this.depth = this.y; + onstep: |- + this.move(); + + if (ct.place.occupied(this, 'Solid')) { + this.x = this.xprev; + this.y = this.yprev; + this.direction += 180; + } + + if (this.hspeed > 0) { + this.scale.x = 1; + } else { + this.scale.x = -1; + } + ondraw: '' + ondestroy: '' + texture: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + extends: + ctype: Monster + uid: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + lastmod: 1596073035148 + - name: Wall + depth: 0 + oncreate: this.depth = this.y; + onstep: this.move(); + ondraw: '' + ondestroy: '' + texture: 387786ba-1c6d-4020-8aa5-1268faa5cbeb + extends: + ctype: Solid + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + lastmod: 1596073038085 + - name: Monster_Red + depth: 0 + oncreate: this.inherit.onCreate(); + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 15293c51-6bdc-4a62-941e-0b08656796a7 + extends: + ctype: Monster + inheritedType@@type: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + uid: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + lastmod: 1596073030644 + - name: Monster_Green + depth: 0 + oncreate: this.inherit.onCreate(); + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 5ae654a1-1d13-4f67-ac3d-cccec7b01cc4 + extends: + ctype: Monster + inheritedType@@type: 4af80a30-74f1-4a68-b7e4-286c7b74e76e + uid: 78739be5-145e-45aa-8e4b-473b92c19551 + lastmod: 1596073032003 + - name: Monster_Red_Squished + depth: 0 + oncreate: |- + this.inherit.onCreate(); + this.scale.y = 0.5; + this.speed = 20; + onstep: this.inherit.onStep(); + ondraw: '' + ondestroy: '' + texture: 15293c51-6bdc-4a62-941e-0b08656796a7 + extends: + inheritedType@@type: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + ctype: Monster + uid: 48c1951e-071c-47ef-a710-ef2ef0978017 + lastmod: 1596073029453 +sounds: [] +styles: [] +rooms: + - name: Room_428ea16b4401 + oncreate: >- + console.log('Each bot should run from wall to wall, and everything below + this text should be green.'); + + ct.assert( + ct.inherit.isParent('AbstractMonster', 'Monster_Red_Squished'), + 'ct.inherit.isParent works with two types (two strings)' + ); + + ct.assert( + ct.inherit.isChild('Monster_Green', 'AbstractMonster'), + 'ct.inherit.isChild works with two types (two strings)' + ); + + ct.assert( + ct.inherit.list('AbstractMonster').length === 3, + 'ct.inherit.list gets all the monsters' + ); + + ct.assert( + ct.inherit.isChild(ct.types.list['Monster_Green'][0], 'AbstractMonster'), + 'ct.inherit.isChild works against a copy and a type' + ); + + ct.assert( + ct.inherit.isChild(ct.types.list['Monster_Red_Squished'][0], ct.types.list['Monster_Red'][0]), + 'ct.inherit.isChild works against two copies' + ); + + ct.assert( + ct.types.list['Monster_Red_Squished'][0].depth !== 0, + 'Monster_Red_Squished has a proper depth, which means that its grandparent\'s code was executed' + ); + + ct.assert.summary(); + onstep: '' + ondraw: '' + onleave: '' + width: 700 + height: 600 + backgrounds: [] + copies: + - x: 0 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 70 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 140 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 210 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 280 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 420 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 490 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 560 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 0 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 70 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 140 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 210 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 280 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 350 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 420 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 630 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 70 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 140 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 210 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 280 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 350 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 420 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 0 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 70 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 140 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 210 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 280 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 420 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 490 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 560 + 'y': 490 + uid: 5d8f84e7-0b1f-4b3e-8d1a-33d12038ce3d + - x: 350 + 'y': 210 + uid: 78739be5-145e-45aa-8e4b-473b92c19551 + - x: 441 + 'y': 380 + uid: 8abcc9f5-e912-46a6-9a3f-ac3e254a4b95 + - x: 210 + 'y': 280 + uid: 48c1951e-071c-47ef-a710-ef2ef0978017 + tx: 1 + ty: 0.5 + tiles: + - depth: -10 + tiles: [] + uid: bf117e14-2f8b-4457-b418-428ea16b4401 + thumbnail: 428ea16b4401 + gridX: 70 + gridY: 70 +actions: [] +emitterTandems: [] +starting: 0 +settings: + authoring: + author: CoMiGo Games + site: 'https://ctjs.rocks/' + title: Inheritance test + version: + - 0 + - 0 + - 0 + versionPostfix: '' + rendering: + usePixiLegacy: true + maxFPS: 60 + pixelatedrender: false + highDensity: true + desktopMode: maximized + export: + windows: true + linux: true + mac: true + branding: + icon: -1 + accent: '#446adb' + invertPreloaderScheme: true + fps: 30 +fonts: [] +scripts: [] diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png new file mode 100644 index 0000000000000000000000000000000000000000..7105328fdf38b941ac51e9f63905c2e51715c134 GIT binary patch literal 2475 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 literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ddf4f8de62cb88701a5884b87e6b6d51644590 GIT binary patch literal 2201 zcmV;K2xj+*P)tsI|{Mukc+sLLNtsv+$7-$NYKl?MP!lZ)A z+A`n|f*?&)1;Hl7gh10kNJW~^gpg1gR&r@dOPe%p5|TPM$CvMR(0ekQxN%}bfpBEgPm~ z!Eu~59#r7vjKS|S;L)_zZOoVijd2d{F`)VU^l#U7-Lt_L&b`7$&VQjn73`0uWgm$r zl5mAs-2p|MtttwF0sop#WQq#SAB3tK*91og8$Ea8Nz1Z+KOax3YudR;i_6ObQ21KS z0PqAW9IhKD*vR)j|4J;8{O+bx`DFn08wR|dRhmJTy|}7D3sk(uhE5;vikW6-kp#Hr zV+^kshVEVSKD2?itl|xJ?OV@uIYMkp02Ecb$ofw`-NnGRB>)g?y8|Ff02I{(D94yf zn6MpzGF^i!fcc~aP2mU{9yw;(F8p2p{Q;DB zbb|ZK0mp%P=Ne|-`VrtdTh|1)5-eICB>BN>06dh;x- z;ejm-KT9wmD1-}VY8;UZ-v%6O3j+W^soD)a^f55igXHxdShok4 zb%NfOc4+kvz>P-{fB#*G7Wz(?Dvu94P=i3gG1o4L*i?9|!;7U2r80ux%Tbz4S$k-V{9^Exkwj z+{GpkH66U(7s23^iKOGg5#%JZCYEc)gD&Lr`QY_>mwoUgc!+161EC59&op}AQyB{0 z5a~Phbk~2T%+Bv$oR zn50a09}EVe>v~%1WCpu;@6JmLG$wE+q#|Gx=4D6%@Z2XK>gu0bcp4KzV0wBQ@pwF! zKtA+10kL!EPN?ca*H=*#R99E0|DS8)mk#L&RU5gc9H*X0NH0a9G0<~h@4|jPmgq#GJM59qTcQF7I87LHbJo$Yg?K8~=o@mtbv6kF(KJWp{ngCtw z;K76G5Jij=BNQph%gYf8g)+u&+_-`9@o~vuYin!9*zoW$MnQmjLsk|i@53cK;VXdvL zpU^bz&s3Pv0xE-NXJ^-?9w3iTK7k~jYJyM}xog+16sYPT8^eHq^bRczI+z%u`QrTp#^N4ZECx{mMe4Y$u z36NBgm1{2{#J_FZ?vQ=TW5ob3gcgGh021N^p!w)jX=*jgF##m+!Tp&P*adLpE%Px0ek~lF=5+}y9HEB{2 zfc7I0R$$(7U6%@xKM5gzIW{)d-PhMA9m(o{gi|0OHp@0805*#xHtHTF4cI8;H_N%( bUIYFILXGOi+rVN`00000NkvXXu0mjf47v~6 literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png b/app/data/ct.libs/inherit/InheritanceTest/img/i15293c51-6bdc-4a62-941e-0b08656796a7.png_prev@2.png new file mode 100644 index 0000000000000000000000000000000000000000..7a2760d4ea62386527a730b3e081b1962df7d353 GIT binary patch literal 4286 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 bTv0fw?|pue*me8gUjoq8e56sWW*`24ZNmt@ literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png b/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png new file mode 100644 index 0000000000000000000000000000000000000000..968f914c76abb8da2f1fea260a6507afcc595c4c GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^Za|#L!3-ofa4ii5QY`6?zK#qG8~eHcB(ehe_5nU2 zu8U@F-gM~j;)Pqb967pT$IdzB>$V?1v1HY@1w9+r?c2Zk@R3Dxx6G?wziiX?Rl9cY zJb7yQ_8q(C9-C9VZujXk%Nn6*Ol(dGR`ypl2KN&}ns=BzN#R@bt=-{tI- z_%y}UGBWX@$}^+%rKNWksvOr;wfy|_u*lM~t{_WSO}?vLB}FVdQ&MBb@0QS7AI{*Lx literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png b/app/data/ct.libs/inherit/InheritanceTest/img/i387786ba-1c6d-4020-8aa5-1268faa5cbeb.png_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..2f940a5cac9c4ad7043b431be9c24ac6fb0dba8d GIT binary patch literal 948 zcmV;l155mgP))vaBSAz#JPIO8G#V9e3Puz>=tc0V|G=9U_2SKcV2%o2B6v`Ypf?de zh>1k-E(k`+W@nO}o}O$=&$=;RYbuA$5~;rI<<-{I>sQtF1U7LV-F;>^0q+^~+U<*i znw$)Df5-%|0=NZu22h7fFL68s`4!-2ko>%pfxHLsBE*{@ufpsF*bA^1U@y?i9iN(; zy#RXw_5$P;pA2L#z+Qm80DA%U0_+9Y3$PcU?&H10u@_)3z+Qm7;*){w1=tI)7ho^I zUVyy-dja+Wz1;Dsxx3~C9^ZShEfjF|)_L-RM+Q3f$4B363j#*2pC;Eu208*B+#K5$ z1l+iPg{q8T@# zCsTkrB?J&2Q$PVoKIl!6rw&Va{RJfN5?H-9;g%5~`UJuu1yxqaOa}AQKz~j*EJjE` z9Z`@2GQaw?mO_8huzCX}cz`QD*cHGJeCS9cadGiV61Ic@Cwn!hp!y{$l?oOY7g4X* zk$y{&elhd?-}5R`*F&fVbh(^im)4U#Dh8~u@W0Z<1eg53eil`^DkwN3jt(&IRWUeHLU zHJUR`bd7+~J5OwJD1_Rf@*>(dxca_~F?l7T=b%yHO|4 ztd>D20a=nu?eect=ZbM8=vc!xEc|{S)&439g~En8=jZ3qh#RoYfGSGeW*sIre zqvF@JLlos7?{3ijQjU|g8*o{c{qyEm|5sZHSI(Xx*F^@pWaIDN+fK6YRG3qOi|Hi$ zPI2v_N+oT1JB*wdCf7vBrZW*l^Uhgr(*~qs~gpeAkYtV#dXxNdzOUUeWZ6 zOe)w^Yw>lR-pCis?mC?qrmZ4WQ4U){F9DdMZI>uq6f|zcirGw>{s!cs_4RWMxf$= zBMu)*Y=CCOXdT`f{1Ce{3o~ntUF#s`_#i17yXwNS;eg@1t+y_3uwyu|A+s_TD%FE zX`iY=vOjbm9O9Y}{@zo_Qps@_?IRnz!in+pph(B{r`R19ojYLbWwTvf{d>AHrFec?-{`$r;P;4uvH$pHVl|_QRsu>StP!0x5)= z#!mMU#eaMYg{$W^jYv};F}XX)5@Sec?u(0x`bZcn)i3Q22w^#B1_p{u__BY@y9C=EHV4EMBLL8&G8^yeXLTcCgJk_xX4KR8M$13d#7ek#l*M?BAP2` zG};rC^-8CyD9*e=8RU$oA7_~3ZF={P8nuFZ_U7D)qk{U*^&tyJmgjvayj|WNH@!8d zY=$2mdZyaE+uM3Q{-KM5c`8-KV0q|d7Bj}KOWvbTq0Ut78FdT%v#q3jHTgz-gt{}( z`DlSD{sqj(l2ACwf;-}xlP6ANq)ZIu4d88^=le0=fFV&h*~M%Tcm zyK>!4Yqxrr2PykV^4W{A>Qc0A&8|wTo2&XWC*4B5P)I^qv$b{THLVwF<8$(-8ju5@ z6T6K1jyw6zb}E#EU3s04n|I`DG;{G6H-)x#_EMeW%)mE-9=tjS=EyE!={!i4PnEQK z;nQPcHi!DR{qtvuUDFGlqJ>l4gZHHnwa?-fA63u6lhNWAywc4QRlTf0!;!8_+zsd( zh?zdFQB(G0V>6mDM2~v9lzsV>n!D3M)ICG2`6uqP$!%#ISNlADl!ATSo&EZIn)$S8 z5cARJC=<{Qj|Sl-lq2N#@kadY#v3q`9l$&WTauSf9<>H0S3x8~W{y-x1OXcYrX!FE zzHVx$8m_bh~3VOV2Bou{#@ZF*G{ vEOd{Td=HvQFhHfJN={jLB#5khJ4k7xZ2jwghi literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png b/app/data/ct.libs/inherit/InheritanceTest/img/i5ae654a1-1d13-4f67-ac3d-cccec7b01cc4.png new file mode 100644 index 0000000000000000000000000000000000000000..b4602398abad8ca855da69f61c2904b27cd3a9e0 GIT binary patch literal 2372 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 literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png b/app/data/ct.libs/inherit/InheritanceTest/img/re8d64a493cce.png new file mode 100644 index 0000000000000000000000000000000000000000..127beca923203880912c1152b5423cedbed27d0f GIT binary patch literal 7859 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 XP7XdhQy2&iqiGOVwN*&48xQ{r$t{cH literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/InheritanceTest/img/splash.png b/app/data/ct.libs/inherit/InheritanceTest/img/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..127beca923203880912c1152b5423cedbed27d0f GIT binary patch literal 7859 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 XP7XdhQy2&iqiGOVwN*&48xQ{r$t{cH literal 0 HcmV?d00001 diff --git a/app/data/ct.libs/inherit/README.md b/app/data/ct.libs/inherit/README.md new file mode 100644 index 000000000..83a2c43b9 --- /dev/null +++ b/app/data/ct.libs/inherit/README.md @@ -0,0 +1,38 @@ +This module implements type inheritance for your types! Firstly, enable the module, then select the parent for one of your types. Then, use one of these functions to call the parent's logic: + +* `this.inherit.onCreate();` +* `this.inherit.onStep();` +* `this.inherit.onDraw();` +* `this.inherit.onDestroy();` + +And that's it! + +## Checking whether a copy is a child of a particular type + +There are two methods that are the opposites of each other: + +* `ct.inherit.isChild(copy, assertedParent)`, and +* `ct.inherit.isParent(copy, assertedChild)`. + +The arguments can be either copies or the names of types, or a mix of both. So you can check `ct.inherit.isChild(this, 'Godwoken')` and get a proper result. + +> **Note:** if the two arguments belong to one type, both methods will return `true`. + +## Getting an array of all the copies of a particular parent + +You can get an array of all the copies of a particular type and its children with `ct.inherit.list('typeName')`; + +```js +const monsters = ct.inherit.list('GenericMonster'); +for (const monster of monsters) { + // Let the bloody massacre begin! + monster.kill = true; +} +``` + +> **Warning:** contrary to `ct.types.list['typeName']`, the returned array is not updated and will be a source of memory leaks with deleted copies if not handled properly. Do store the returned value in `var`, `let` or `const`. + +## Things to watch out + +* Events are not inherited by default, so you should explicitly write `this.inherit.onCreate` and other methods where you need them. This is due to the fact that in ct.js v1 each event is still a function, even though an empty one. This behavior will probably be changed in v2. +* Don't make circular references when you form a parenting chain where one child becomes a child of another. They won't work anyways. If you get an error `Maximum call stack size exceeded`, then you do have circular references. \ No newline at end of file diff --git a/app/data/ct.libs/inherit/index.js b/app/data/ct.libs/inherit/index.js new file mode 100644 index 000000000..afa3ff0f0 --- /dev/null +++ b/app/data/ct.libs/inherit/index.js @@ -0,0 +1,50 @@ +ct.inherit = { + isChild(type, assertedType) { + // Get type names from copies + if (type instanceof Copy) { + ({type} = type); + } + if (assertedType instanceof Copy) { + assertedType = assertedType.type; + } + // Throw an error if a particular type does not exist. + if (!(type in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${type} does not exist. A typo?`); + } + if (!(assertedType in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${assertedType} does not exist. A typo?`); + } + // Well, technically a type is not a child of itself, but I suppose you expect to get `true` + // while checking whether a copy belongs to a particular class. + if (type === assertedType) { + return true; + } + let proposedType = ct.types.templates[type].extends.inheritedType; + while (proposedType) { + if (proposedType === assertedType) { + return true; + } + proposedType = ct.types.templates[proposedType].extends.inheritedType; + } + return false; + }, + isParent(type, assertedType) { + return ct.inherit.isChild(assertedType, type); + }, + list(type) { + // Throw an error if a particular type does not exist. + if (!(type in ct.types.templates)) { + throw new Error(`[ct.inherit] The type ${type} does not exist. A typo?`); + } + + // Get a list of all child types to concat their ct.types.lists in one go + const types = []; + for (const i in ct.types.list) { + if (i !== 'BACKGROUND' && i !== 'TILELAYER' && ct.inherit.isParent(type, i)) { + types.push(i); + } + } + + return [].concat(...types.map(t => ct.types.list[t])); + } +}; diff --git a/app/data/ct.libs/inherit/injects/onbeforecreate.js b/app/data/ct.libs/inherit/injects/onbeforecreate.js new file mode 100644 index 000000000..951e0ca63 --- /dev/null +++ b/app/data/ct.libs/inherit/injects/onbeforecreate.js @@ -0,0 +1,40 @@ +if ((this instanceof ct.types.Copy) && this.inheritedType) { + this.inherit = { + onCreate: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onCreate.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onStep: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onStep.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onDraw: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onDraw.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + }, + onDestroy: () => { + const oldType = this.type, + oldInherited = this.inheritedType; + this.type = this.inheritedType; + this.inheritedType = ct.types.templates[this.inheritedType].extends.inheritedType; + ct.types.templates[oldInherited].onDestroy.apply(this); + this.type = oldType; + this.inheritedType = oldInherited; + } + }; +} diff --git a/app/data/ct.libs/inherit/module.json b/app/data/ct.libs/inherit/module.json new file mode 100644 index 000000000..ea2151f50 --- /dev/null +++ b/app/data/ct.libs/inherit/module.json @@ -0,0 +1,15 @@ +{ + "main": { + "name": "Type inheritance", + "version": "1.0.0", + "authors": [{ + "name": "Cosmo Myzrail Gorynych", + "mail": "admin@nersta.ru" + }] + }, + "typeExtends": [{ + "name": "Parent", + "type": "type", + "key": "inheritedType@@type" + }] +} diff --git a/app/data/ct.libs/inherit/types.d.ts b/app/data/ct.libs/inherit/types.d.ts new file mode 100644 index 000000000..034a54449 --- /dev/null +++ b/app/data/ct.libs/inherit/types.d.ts @@ -0,0 +1,75 @@ +declare namespace ct { + /** + * A module that brings inheritance capabilities to your copies. + */ + namespace inherit { + + /** + * Checks whether a first given entity is a child of another. + * You can check one copy against another, or a copy against a type name (a string), + * or the reverse, or even use type names only, meaning that `ct.inherit.isChild(this, 'Godwoken')` + * will work and will return the expected result. + * + * @param {Copy|string} copy The copy to check against, or the name of a type + * @param {Copy|string} assertedParent The copy that may be the parent of `copy`, or the name of a type. + * + * @returns `true` if the type of a first entity is a child type of a second one, + * or if this is one type. Returns `false` otherwise. If you don't want copies + * of one type to match, compare their `type` parameters mnually prior to using `isChild`. + */ + function isChild(copy: Copy | string, assertedParent: Copy | string): boolean; + + /** + * Checks whether a first given entity is a parent of another. + * You can check one copy against another, or a copy against a type name (a string), + * or the reverse, or even use type names only, meaning that `ct.inherit.isParent('AbstractMonster', this)` + * will work and will return the expected result. + * + * @param {Copy|string} copy The copy to check against, or the name of a type + * @param {Copy|string} assertedChild The copy that may be the child of `copy`, or the name of a type. + * + * @returns `true` if the type of a first entity is a parent type of a second one, + * or if this is one type. Returns `false` otherwise. If you don't want copies + * of one type to match, compare their `type` parameters mnually prior to using `isParent`. + */ + function isParent(copy: Copy | string, assertedChild: Copy | string): boolean; + + /** + * Returns an array of all the copies of a particular type and its children. + */ + function list(type: string): Array; + } +} + + +interface Copy { + /** + * This module implements type inheritance for your types. + */ + inherit: { + /** + * Calls the onCreate method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onCreate(): void; + /** + * Calls the onStep method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onStep(): void; + /** + * Calls the onDraw method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onDraw(): void; + /** + * Calls the onDestroy method of this copy's parent + * (in sense of type inheritance, not in sense of scene graph). + * Provided by `ct.inherit` module. + * */ + onDestroy(): void; + } +} \ No newline at end of file diff --git a/app/data/ct.libs/place/module.json b/app/data/ct.libs/place/module.json index 457bfb795..607716bee 100644 --- a/app/data/ct.libs/place/module.json +++ b/app/data/ct.libs/place/module.json @@ -8,31 +8,33 @@ }] }, "fields": [{ + "name": "Partitioning", + "type": "h2" + }, { "name": "Grid size X", "help": "Tells ct.place how to spacially group copies. This should be at least as large as the horizontal side of the biggest colliding sprite of your game.", "key": "gridX", - "id": "gridX", "default": 512, "type": "number" }, { "name": "Grid size Y", "help": "Tells ct.place how to spacially group copies. This should be at least as large as the vertical size of the biggest colliding sprite of your game.", "key": "gridY", - "id": "gridY", "default": 512, "type": "number" }, { "name": "Debug mode", + "type": "h2" + }, { + "name": "Enable", "help": "Displays collision shapes, collision groups and partitions. It will also write additional keys to most colliding objects. Doesn't work on hidden objects.", "key": "debugMode", - "id": "debugMode", "default": false, "type": "checkbox" }, { "name": "Debug text size", "help": "", "key": "debugText", - "id": "debugText", "default": 16, "type": "number" }], diff --git a/app/data/i18n/English.json b/app/data/i18n/English.json index e5cfbd254..b3697fc7e 100644 --- a/app/data/i18n/English.json +++ b/app/data/i18n/English.json @@ -10,6 +10,7 @@ "apply": "Apply", "cancel": "Cancel", "cannotBeEmpty": "This cannot be empty", + "clear": "Clear", "confirmDelete": "Are you sure you want to delete {0}? It cannot be undone.", "contribute": "Contribute", "copy": "Copy", @@ -177,7 +178,7 @@ "openSpaceShooterTutorial": "Learn how to make a space shooter", "openPlatformerTutorial": "Learn how to make a platformer", "openJettyCatTutorial": "Learn how to make a Jetty Cat", - "doNothing": "Skip this screen", + "doNothing": "Skip this screen and make a great game!", "showOnboardingCheckbox": "Show this screen when creating a new project" }, "settings": { diff --git a/app/data/i18n/Russian.json b/app/data/i18n/Russian.json index 71bd7bad6..ca0edc8d0 100644 --- a/app/data/i18n/Russian.json +++ b/app/data/i18n/Russian.json @@ -10,6 +10,7 @@ "apply": "Применить", "cancel": "Отмена", "cannotBeEmpty": "Не может быть пустым", + "clear": "Очистить", "confirmDelete": "Вы уверены, что хотите удалить {0}? Отменить удаление будет невозможно!", "contribute": "Внести вклад в разработку", "copy": "Копировать", diff --git a/app/package-lock.json b/app/package-lock.json index dcf476895..1e99c58b4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2639,9 +2639,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.defaults": { "version": "4.2.0", diff --git a/package-lock.json b/package-lock.json index a5a3913fa..ec32c2bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,7 +769,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -4262,7 +4263,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "micromatch": { "version": "3.1.10", @@ -6114,9 +6116,9 @@ "integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==" }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._baseassign": { "version": "3.2.0", @@ -8737,7 +8739,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, diff --git a/src/node_requires/exporter/rooms.js b/src/node_requires/exporter/rooms.js index d5776bf37..8ba9c287f 100644 --- a/src/node_requires/exporter/rooms.js +++ b/src/node_requires/exporter/rooms.js @@ -1,4 +1,5 @@ const glob = require('./../glob'); +const {getUnwrappedExtends} = require('./utils'); const getStartingRoom = proj => { let [startroom] = proj.rooms; // picks the first room by default @@ -35,7 +36,7 @@ const stringifyRooms = proj => { const layer = { depth: tileLayer.depth, tiles: [], - extends: tileLayer.extends + extends: tileLayer.extends ? getUnwrappedExtends(tileLayer.extends) : {} }; for (const tile of tileLayer.tiles) { for (let x = 0; x < tile.grid[2]; x++) { diff --git a/src/node_requires/exporter/types.js b/src/node_requires/exporter/types.js index 493004709..e9048e1cf 100644 --- a/src/node_requires/exporter/types.js +++ b/src/node_requires/exporter/types.js @@ -1,4 +1,5 @@ -const textures = require('../resources/textures'); +const {getTextureFromId} = require('../resources/textures'); +const {getUnwrappedExtends} = require('./utils'); const stringifyTypes = function (proj) { /* Stringify types */ @@ -8,7 +9,7 @@ const stringifyTypes = function (proj) { types += ` ct.types.templates["${type.name}"] = { depth: ${type.depth}, - ${type.texture !== -1 ? 'texture: "' + textures.getTextureFromId(type.texture).name + '",' : ''} + ${type.texture !== -1 ? 'texture: "' + getTextureFromId(type.texture).name + '",' : ''} onStep: function () { ${type.onstep} }, @@ -21,7 +22,7 @@ ct.types.templates["${type.name}"] = { onCreate: function () { ${type.oncreate} }, - extends: ${JSON.stringify(type.extends || {})} + extends: ${type.extends ? JSON.stringify(getUnwrappedExtends(type.extends), null, 4) : '{}'} }; ct.types.list['${type.name}'] = [];`; } diff --git a/src/node_requires/exporter/utils.js b/src/node_requires/exporter/utils.js new file mode 100644 index 000000000..5beecce5f --- /dev/null +++ b/src/node_requires/exporter/utils.js @@ -0,0 +1,51 @@ +/** + * Creates a copy of an extends object, turning UIDs of resources into the names of these resources. + * Understands notations of `name@@type` and `name@@texture`. + * + * @param {object} exts A flat map of extends. + * + * @returns {object} An object with unwrapped extends. + */ +const {getTypeFromId} = require('./../resources/types'); +const {getTextureFromId} = require('./../resources/textures'); +const getUnwrappedExtends = function getUnwrappedExtends(exts) { + const out = {}; + for (const i in exts) { + const split = i.split('@@'); + if (split.length === 1) { + out[i] = exts[i]; + continue; + } + const postfix = split.pop(); + const key = split.join('@@'); + if (postfix === 'type') { + try { + out[key] = getTypeFromId(exts[i]).name; + } catch (e) { + alertify.error(`Could not resolve UID ${exts[i]} for field ${key} as a type. Returning -1.`); + console.error(e); + // eslint-disable-next-line no-console + console.trace(); + out[key] = -1; + } + } else if (postfix === 'texture') { + try { + out[key] = getTextureFromId(exts[i]).name; + } catch (e) { + alertify.error(`Could not resolve UID ${exts[i]} for field ${key} as a texture. Returning -1.`); + console.error(e); + // eslint-disable-next-line no-console + console.trace(); + out[key] = -1; + } + } else { + // Seems to be an unsupported postfix. Output the old key as is. + out[i] = exts[i]; + } + } + return out; +}; + +module.exports = { + getUnwrappedExtends +}; diff --git a/src/node_requires/resources/types/defaultType.js b/src/node_requires/resources/types/defaultType.js index 7a8be4b15..e7fc9e4f3 100644 --- a/src/node_requires/resources/types/defaultType.js +++ b/src/node_requires/resources/types/defaultType.js @@ -15,6 +15,7 @@ module.exports = { get() { return ({ ...defaultTypeTemplate, + extends: {}, uid: generateGUID() }); } diff --git a/src/node_requires/resources/types/index.js b/src/node_requires/resources/types/index.js index e53ebb289..11e394066 100644 --- a/src/node_requires/resources/types/index.js +++ b/src/node_requires/resources/types/index.js @@ -1,6 +1,6 @@ const getDefaultType = require('./defaultType').get; -const createNewType = function (name) { +const createNewType = function createNewType(name) { const type = getDefaultType(); if (name) { type.name = String(name); @@ -10,7 +10,40 @@ const createNewType = function (name) { return type; }; +/** + * Gets the ct.js type object by its id. + * @param {string} id The id of the ct.js type + * @returns {IType} The ct.js type object + */ +const getTypeFromId = function getTypeFromId(id) { + const type = global.currentProject.types.find(t => t.uid === id); + if (!type) { + throw new Error(`Attempt to get a non-existent type with ID ${id}`); + } + return type; +}; + +/** + * Retrieves the full path to a thumbnail of a given type. + * @param {string|IType} type Either the id of the type, or its ct.js object + * @param {boolean} [x2] If set to true, returns a 128x128 image instead of 64x64. + * @param {boolean} [fs] If set to true, returns a file system path, not a URI. + * @returns {string} The full path to the thumbnail. + */ +const getTypePreview = function getTypePreview(type, x2, fs) { + const {getTexturePreview} = require('./../textures'); + if (typeof type === 'string') { + type = getTypeFromId(type); + } + if (type === -1) { + return getTexturePreview(-1, x2, fs); + } + return getTexturePreview(type.texture, x2, fs); +}; + module.exports = { getDefaultType, + getTypeFromId, + getTypePreview, createNewType }; diff --git a/src/riotTags/modules-panel.tag b/src/riotTags/modules-panel.tag index 6d793dc61..208dbdff8 100644 --- a/src/riotTags/modules-panel.tag +++ b/src/riotTags/modules-panel.tag @@ -90,48 +90,7 @@ modules-panel.panel.view code {currentModuleLicense} #modulesettings.tabbed.nbt(show="{tab === 'modulesettings'}" if="{currentModule.fields && currentModuleName in global.currentProject.libs}") - dl(each="{field in currentModule.fields}") - dt - label.block.checkbox(if="{field.type === 'checkbox'}") - input( - type="checkbox" - checked="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - | {field.name} - span(if="{field.type !== 'checkbox'}") - | {field.name} - dd - textarea( - if="{field.type === 'textfield'}" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - input( - if="{field.type === 'number'}" - type="number" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - label.block.checkbox(if="{field.type === 'radio'}" each="{option in field.options}") - input( - type="radio" - value="{option.value}" - checked="{global.currentProject.libs[currentModuleName][field.id] === option.value}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - | {option.name} - div(class="desc" if="{option.help}") - raw(ref="raw" content="{md.render(option.help)}") - input( - if="{['checkbox', 'number', 'textfield', 'radio'].indexOf(field.type) === -1}" - type="text" - value="{global.currentProject.libs[currentModuleName][field.id]}" - onchange="{wire('global.currentProject.libs.' + escapeDots(currentModuleName) + '.' + field.id)}" - ) - //- That's a bad idea!!! - div(class="desc" if="{field.help}") - raw(ref="raw" content="{md.render(field.help)}") + extensions-editor(customextends="{currentModule.fields}" entity="{global.currentProject.libs[currentModuleName]}") #modulehelp.tabbed.nbt(show="{tab === 'modulehelp'}" if="{currentModuleDocs}") raw(ref="raw" content="{currentModuleDocs}") #modulelogs.tabbed.nbt(show="{tab === 'modulelogs'}" if="{currentModuleLogs}") diff --git a/src/riotTags/project-settings/project-settings.tag b/src/riotTags/project-settings/project-settings.tag index 21f992d08..c50e87f73 100644 --- a/src/riotTags/project-settings/project-settings.tag +++ b/src/riotTags/project-settings/project-settings.tag @@ -1,6 +1,6 @@ project-settings.panel.view.pad.flexrow - - var tabs = ['authoring', 'actions', 'branding', 'rendering', 'scripts']; + var tabs = ['authoring', 'actions', 'branding', 'scripts', 'rendering']; var iconMap = { authoring: 'edit', actions: 'airplay', diff --git a/src/riotTags/rooms/room-tile-editor.tag b/src/riotTags/rooms/room-tile-editor.tag index 20770ec13..e1a3ef2f9 100644 --- a/src/riotTags/rooms/room-tile-editor.tag +++ b/src/riotTags/rooms/room-tile-editor.tag @@ -28,7 +28,7 @@ room-tile-editor.room-editor-Tiles.tabbed.tall.flexfix svg.feather use(xlink:href="data/icons.svg#plus") .block - extensions-editor(type="tileLayer" entity="{parent.currentTileLayer.extends}" compact="yep") + extensions-editor(type="tileLayer" entity="{parent.currentTileLayer.extends}" compact="yep" wide="sure") texture-selector(ref="tilesetPicker" if="{pickingTileset}" oncancelled="{onTilesetCancel}" onselected="{onTilesetSelected}") script. this.parent.tileX = 0; diff --git a/src/riotTags/shared/asset-viewer.tag b/src/riotTags/shared/asset-viewer.tag index 262ab2f00..8c55ceedf 100644 --- a/src/riotTags/shared/asset-viewer.tag +++ b/src/riotTags/shared/asset-viewer.tag @@ -3,8 +3,10 @@ @slot Can use nested tags. Yields the passed markup as a header of an asset viewer. + @attribute class (string) This tag has its own CSS classes, but allows arbitrary ones added as an attribute. + @attribute namespace (string) A unique namespace used to store settings. Fallbacks to 'default'. @attribute vocspace (string) @@ -124,4 +126,6 @@ asset-viewer.flexfix(class="{opts.namespace} {opts.class}") this.switchLayout = () => { const key = this.opts.namespace ? (this.opts.namespace + 'Layout') : 'defaultAssetLayout'; localStorage[key] = localStorage[key] === 'list' ? 'grid' : 'list'; - }; \ No newline at end of file + }; + + this.updateList(); \ No newline at end of file diff --git a/src/riotTags/shared/extensions-editor.tag b/src/riotTags/shared/extensions-editor.tag index b58928314..ff08ecafe 100644 --- a/src/riotTags/shared/extensions-editor.tag +++ b/src/riotTags/shared/extensions-editor.tag @@ -5,45 +5,103 @@ @attribute entity (riot object) An object to which apply editing to. @attribute type (string, 'type'|'tileLayer') - The type of the edited asset. + The type of the edited asset. Not needed if customextends is set. @attribute [compact] (atomic) Whether to use a more compact layout, replacing full-text hints with icons and using more compact classes for fields. + @attribute [wide] (atomic) + Whether to prefer a full-width layout. Useful for making neat columns of editable fields. @attribute [customextends] (riot Array) Instead of reading modules' directory, use these extends specification instead. Useful for quickly generating markup for built-in fields. + Extensions are an array of objects. The format of an extension is as following: + + declare interface IExtensionField { + name: string, // the displayed name. + // Below 'h1', 'h2', 'h3', 'h4' are purely decorational, for grouping fields. Others denote the type of an input field. + type: 'h1' | 'h2' | 'h3' | 'h4' | 'text' | 'textbox' | 'number' | 'checkbox' | 'radio' | 'texture' | 'type', + key?: string, // the name of a JSON key to write into the `opts.entity`. Not needed for hN types, but required otherwise + // The key may have special suffixes that tell the exporter to unwrap foreign keys (resources' UIDs) into asset names. + // These are supposed to always be used with `'type'` and `'texture'` input types. + // Example: 'enemyClass@@type', 'background@@texture'. + default?: any, // the default value; it is not written to the `opts.entity`, but is shown in inputs. + help?: string, // a text label describing the purpose of a field + options?: Array<{ // Used with type === 'radio'. + value: any, + name: string, + help?: string + }>, + collect?: boolean, // Whether to collect values and suggest them later as an auto-completion results. (Not yet implemented) + collectScope?: string // The name of a category under which to store suggestions from `collect`. + } + extensions-editor virtual(each="{ext in extensions}") - label - div(class="{parent.opts.compact ? 'flexrow' : 'block'}") - input.nogrow( - type="checkbox" + h1(if="{ext.type === 'h1'}") {ext.name} + h2(if="{ext.type === 'h2'}") {ext.name} + h3(if="{ext.type === 'h3'}") {ext.name} + h4(if="{ext.type === 'h4'}") {ext.name} + dl(class="{compact: compact}" if="{['h1', 'h2', 'h3', 'h4'].indexOf(ext.type) === -1}") + dt + label.block.checkbox(if="{ext.type === 'checkbox'}") + input.nogrow( + if="{ext.type === 'checkbox'}" + type="checkbox" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + span {ext.name} + hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") + span(if="{ext.type !== 'checkbox'}") + b {ext.name} + b : + hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") + dd + texture-input( + if="{ext.type === 'texture'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + val="{parent.opts.entity[ext.key] || ext.default}" + onselected="{writeUid(ext.key)}" + ) + type-input( + if="{ext.type === 'type'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + val="{parent.opts.entity[ext.key] || ext.default}" + onselected="{writeUid(ext.key)}" + ) + input( + if="{ext.type === 'text'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + type="text" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + textarea( + if="{ext.type === 'textfield'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + value="{parent.opts.entity[ext.key] || ext.default}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + input( + if="{ext.type === 'number'}" + class="{compact: parent.opts.compact, wide: parent.opts.wide}" + type="number" value="{parent.opts.entity[ext.key] || ext.default}" onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'checkbox'}" ) - b.nogrow {ext.name} - b.nogrow(if="{ext.type !== 'checkbox'}") : - .filler(if="{parent.opts.compact}") - hover-hint(if="{ext.help && parent.opts.compact}" text="{ext.help}") - input.wide( - class="{compact: parent.opts.compact}" - type="text" - value="{parent.opts.entity[ext.key] || ext.default}" - onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'text'}" - ) - input.wide( - class="{compact: parent.opts.compact}" - type="number" - value="{parent.opts.entity[ext.key] || ext.default}" - onchange="{wire('this.opts.entity.'+ ext.key)}" - if="{ext.type === 'number'}" - ) - .dim(if="{ext.help && !parent.opts.compact}") {ext.help} + label.block.checkbox(if="{ext.type === 'radio'}" each="{option in ext.options}") + input( + type="radio" + value="{option.value}" + checked="{parent.parent.opts.entity[ext.key] === option.value}" + onchange="{wire('this.opts.entity.'+ ext.key)}" + ) + | {option.name} + div.desc(if="{option.help}") {option.help} + .dim(if="{ext.help && !parent.opts.compact}") {ext.help} script. const libsDir = './data/ct.libs'; const fs = require('fs-extra'), @@ -71,6 +129,26 @@ extensions-editor }); } }; + + this.on('update', () => { + if (!this.opts.entity) { + console.error('extension-editor tag did not receive its `entity` object for editing!'); + console.log(this); + } + if (this.opts.customextends && this.opts.customextends !== this.extensions) { + this.extensions = this.opts.customextends; + } + }); + + this.writeUid = field => obj => { + if (obj) { + this.opts.entity[field] = obj.uid; + } else { + this.opts.entity[field] = -1; + } + this.update(); + }; + window.signals.on('modulesChanged', this.refreshExtends); this.on('unmount', () => { window.signals.off('modulesChanged', this.refreshExtends); diff --git a/src/riotTags/shared/type-input.tag b/src/riotTags/shared/type-input.tag new file mode 100644 index 000000000..ffc01846c --- /dev/null +++ b/src/riotTags/shared/type-input.tag @@ -0,0 +1,66 @@ +// + A button that allows to pick a ct.js type, showing current selection's miniature. + + @attribute showempty (any string or empty) + If set, allows to pick no type. + @attribute val (type's uid or -1) + Current input's value + @attribute header (string) + Passed to the type selector, showing a header in the top-left corner. + @attribute onselected (riot function) + A callback that is called when a type is selected, or when no type was selected. + Passes the type's object and its ID as two arguments. +type-input + .flexrow + img(src="{getTypePreview(val || -1)}" onclick="{openSelector}") + input.wide( + type="text" readonly + value="{val && val !== -1 ? getTypeFromId(val).name : voc.select}" + onclick="{openSelector}" + ) + .spacer(if="{val && val !== -1}") + button.nmr.square.inline(if="{val && val !== -1}" title="{voc.clear}" onclick="{clearInput}") + svg.feather + use(xlink:href="data/icons.svg#x") + type-selector( + if="{selectingType}" + onselected="{onSelected}" + oncancelled="{onCancelled}" + header="{opts.header}" + ) + script. + this.namespace = 'common'; + this.mixin(window.riotVoc); + + const {getTypePreview, getTypeFromId} = require('./data/node_requires/resources/types'); + this.getTypePreview = getTypePreview; + this.getTypeFromId = getTypeFromId; + + this.val = this.opts.val || -1; + this.openSelector = () => { + this.selectingType = true; + }; + this.onSelected = type => () => { + if (this.opts.onselected) { + this.opts.onselected(type, type.uid); + } + this.val = type.uid; + this.selectingType = false; + this.update(); + }; + this.clearInput = () => { + if (this.opts.onselected) { + this.opts.onselected(null, -1); + } + this.val = -1; + }; + this.onCancelled = () => { + this.selectingType = false; + this.update(); + }; + + this.on('update', () => { + if (this.val !== this.opts.val) { + this.val = this.opts.val; + } + }); \ No newline at end of file diff --git a/src/riotTags/shared/type-selector.tag b/src/riotTags/shared/type-selector.tag new file mode 100644 index 000000000..7ff6a4156 --- /dev/null +++ b/src/riotTags/shared/type-selector.tag @@ -0,0 +1,26 @@ +// + Allows users to pick a type object. + + @attribute header (any string or empty) + An optional header shown in the top-left corner + @attribute onselected (riot function) + A two-fold function (type => e => {…}). Calls the funtion with the selected + ct type as the only argument in the first function, and MouseEvent in the second. + @attribute oncancelled (riot function) + Calls the funtion when a user presses the "Cancel" button. Passes no arguments. + +type-selector.panel.view.flexfix + .flexfix-body + asset-viewer( + collection="{global.currentProject.types}" + namespace="types" + click="{opts.onselected}" + thumbnails="{thumbnails}" + ref="types" + class="tall" + ) + h1(if="{opts.header}") {opts.header} + .flexfix-footer(if="{opts.oncancelled}") + button(onclick="{opts.oncancelled}") {window.languageJSON.common.cancel} + script. + this.thumbnails = require('./data/node_requires/resources/types').getTypePreview; diff --git a/src/riotTags/type-editor.tag b/src/riotTags/type-editor.tag index 91d6d3307..2ae3c6a3e 100644 --- a/src/riotTags/type-editor.tag +++ b/src/riotTags/type-editor.tag @@ -12,7 +12,7 @@ type-editor.panel.view.flexrow b {voc.depth} input.wide(type="number" onchange="{wire('this.type.depth')}" value="{type.depth}") .flexfix-body - extensions-editor(type="type" entity="{type.extends}") + extensions-editor(type="type" entity="{type.extends}" wide="yep" compact="probably") br br docs-shortcut(path="/ct.types.html" button="true" wide="true" title="{voc.learnAboutTypes}") diff --git a/src/riotTags/types-panel.tag b/src/riotTags/types-panel.tag index 1702a89ac..d886d7129 100644 --- a/src/riotTags/types-panel.tag +++ b/src/riotTags/types-panel.tag @@ -24,9 +24,7 @@ types-panel.panel.view this.sort = 'name'; this.sortReverse = false; - this.thumbnails = type => (type.texture !== -1 ? - `${glob.texturemap[type.texture].src.split('?')[0]}_prev.png?cache=${this.getTypeTextureRevision(type)}` : - 'data/img/notexture.png'); + this.thumbnails = require('./data/node_requires/resources/types').getTypePreview; this.setUpPanel = () => { this.fillTypeMap(); diff --git a/src/styl/tags/shared/extensions-editor.styl b/src/styl/tags/shared/extensions-editor.styl new file mode 100644 index 000000000..f390c8072 --- /dev/null +++ b/src/styl/tags/shared/extensions-editor.styl @@ -0,0 +1,5 @@ +extensions-editor + type-selector.view, texture-selector.view + position fixed + z-index 9 + cursor default \ No newline at end of file diff --git a/src/styl/tags/shared/texture-selector.styl b/src/styl/tags/shared/texture-selector.styl new file mode 100644 index 000000000..26e73fb46 --- /dev/null +++ b/src/styl/tags/shared/texture-selector.styl @@ -0,0 +1,4 @@ +texture-selector + z-index 4 + position fixed !important + padding 1em \ No newline at end of file diff --git a/src/styl/tags/shared/type-input.styl b/src/styl/tags/shared/type-input.styl new file mode 100644 index 000000000..410ce1558 --- /dev/null +++ b/src/styl/tags/shared/type-input.styl @@ -0,0 +1,5 @@ +type-input > .flexrow > img + width 2rem + height @width + align-self center + margin-right 0.5rem \ No newline at end of file diff --git a/src/styl/tags/shared/type-selector.styl b/src/styl/tags/shared/type-selector.styl new file mode 100644 index 000000000..601a394c5 --- /dev/null +++ b/src/styl/tags/shared/type-selector.styl @@ -0,0 +1,2 @@ +type-selector + @extends texture-selector \ No newline at end of file diff --git a/src/styl/tags/textures/texture-selector.styl b/src/styl/tags/textures/texture-selector.styl deleted file mode 100644 index e86b75909..000000000 --- a/src/styl/tags/textures/texture-selector.styl +++ /dev/null @@ -1,2 +0,0 @@ -texture-selector - position fixed !important \ No newline at end of file diff --git a/src/styl/tags/textures/textures-panel.styl b/src/styl/tags/textures/textures-panel.styl index 3054ca09c..d602d4ba6 100644 --- a/src/styl/tags/textures/textures-panel.styl +++ b/src/styl/tags/textures/textures-panel.styl @@ -1,5 +1,2 @@ -textures-panel, texture-selector - padding 1em - -texture-selector - z-index 4 +textures-panel + padding 1em \ No newline at end of file