├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE.txt ├── README.md ├── package.json ├── player ├── gulpfile.js ├── index.ts └── tsconfig.json ├── plugins └── default │ ├── blob │ ├── data │ │ ├── BlobAsset.ts │ │ └── index.ts │ ├── editors │ │ └── blob │ │ │ ├── index.pug │ │ │ ├── index.styl │ │ │ └── index.ts │ ├── index.d.ts │ ├── package.json │ ├── public │ │ ├── editors │ │ │ └── blob │ │ │ │ ├── icon.svg │ │ │ │ └── manifest.json │ │ └── locales │ │ │ └── en │ │ │ ├── blobEditor.json │ │ │ └── plugin.json │ └── tsconfig.json │ ├── export │ ├── build │ │ ├── Love2dBuildSettings.d.ts │ │ ├── Love2dBuildSettingsEditor.ts │ │ ├── buildLove2d.ts │ │ └── index.ts │ ├── index.d.ts │ ├── package.json │ ├── public │ │ └── locales │ │ │ └── en │ │ │ ├── buildSettingsEditors.json │ │ │ └── builds.json │ └── tsconfig.json │ └── lua │ ├── data │ ├── LuaAsset.ts │ └── index.ts │ ├── editors │ └── lua │ │ ├── index.pug │ │ ├── index.styl │ │ └── index.ts │ ├── index.d.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── editors │ │ └── lua │ │ │ ├── icon.svg │ │ │ └── manifest.json │ └── locales │ │ └── en │ │ ├── plugin.json │ │ └── scriptEditor.json │ └── tsconfig.json ├── public ├── index.html └── locales │ ├── de │ └── system.json │ ├── en │ └── system.json │ └── it │ └── system.json └── server ├── gulpfile.js ├── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | player/**/*.js 3 | server/**/*.js 4 | 5 | public/index.js 6 | public/plugins.json 7 | public/templates.json 8 | 9 | plugins/**/*.html 10 | plugins/**/*.css 11 | plugins/**/*.js 12 | 13 | !gulpfile.js 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "plugins/common"] 2 | path = plugins/common 3 | url = https://github.com/superpowers/superpowers-common-plugins.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: "node" 4 | # Skip "npm install" since "npm run build" takes care of it below 5 | install: true 6 | before_script: 7 | - cd .. 8 | - git clone https://github.com/superpowers/superpowers-core.git 9 | - cd superpowers-core 10 | - mkdir systems 11 | - mv ../superpowers-love2d systems/love2d 12 | - ls -l systems 13 | cache: 14 | directories: 15 | - node_modules 16 | script: 17 | - npm run build 18 | notifications: 19 | webhooks: 20 | urls: 21 | - https://webhooks.gitter.im/e/44f5607466509af53a93 22 | on_success: change 23 | on_failure: always 24 | on_start: never 25 | before_deploy: 26 | - npm run package love2d 27 | deploy: 28 | provider: releases 29 | api_key: 30 | secure: yo8u4It6VKNjZRYU2RDignqnCeYB8X/kBWFGQNV+S/7UbBYXi8LR0IUyPmMeE7ZGeaBGWYLBKFa8ovZes8b2blNiyR1s0OUEisSBpKtzeBta0+J2xV0SDlt/a95Bb/CEMBr3ko/MjyO7Fo3Y0IZuAmBImSNdwEaRqVE+YPtc4PhFio5pZK22YgN3Ccu9O9jDYzIMNpvxQn//EDsW45d4g1bquLGFrjrC5+Im756vqdkfm6txSm5CnUKai68NGBAZ4OZ9pMxYkSQaAMbKUr/qsi6/5JTM71Gjfwsyw7KqgqevjQrmDmNucNnMw0jEKltSQe3CrenOPsSjlvMMT+h5+uwCAPVChmYLqXNpC5Fy2G99QjEbbqGJh67DHRfr68s3Jx4Jlry9jK7aqHZUm7B3OVapmuETyEbwVclz93jdJ8HXnKQhnYaJo3/1FYahRTUsrIjIVK2ay2M0KAtr5zgGSeNspIGeRSRlkSK8OO+n9wunaemFDzxHQnapdQl3Xe7sgg0veramzKkxZ0XQJP26KDep3tCCgRw/uOk/E17Ss9F9x1ZpNEvusEoD5uFqQRwWFEJeGYmH6tsv7ck1BtEQqDHo/XT8l/jV0PtfVGTYGJNB1wwwBApwRQfqomA6ppnltQ42WzYniXKXMl/rTBZ5G2ZysvHQP7WqdnzMjWaLLdI= 31 | file_glob: true 32 | file: "packages/superpowers-love2d-*.zip" 33 | skip_cleanup: true 34 | on: 35 | repo: superpowers/superpowers-love2d 36 | tags: true 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Superpowers is distributed under the ISC license, 2 | in the hope of making it as useful as possible for everyone. 3 | https://en.wikipedia.org/wiki/ISC_license 4 | 5 | We are a welcoming community and we'd love to have you contributing! 6 | https://github.com/superpowers 7 | 8 | ------------------------------------------------------------------------------ 9 | 10 | Copyright © 2014-2016, Sparklin Labs 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 17 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 19 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 20 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 21 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 22 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Superpowers LÖVE 2 | 3 | [![Build Status](https://travis-ci.org/superpowers/superpowers-love2d.svg?branch=master)](https://travis-ci.org/superpowers/superpowers-love2d) 4 | 5 | Make [LÖVE 2D](https://love2d.org/) games in live collaboration with [Superpowers](http://superpowers-html5.com/). 6 | 7 | ![](http://i.imgur.com/yLybycP.gif) 8 | 9 | ## How to install 10 | 11 | For now, you'll need to build it from source. 12 | 13 | You should check out the repository in `superpowers/systems/love2d`. See [Building Superpowers](http://docs.sparklinlabs.com/en/development/building-superpowers). 14 | 15 | Once Superpowers v1.0 is released, installing a new system will be possible in a couple of clicks! 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superpowers-love2d", 3 | "description": "Löve2D system for Superpowers", 4 | "version": "4.0.0", 5 | "license": "ISC", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/superpowers/superpowers-love2d.git" 9 | }, 10 | "superpowers": { 11 | "systemId": "love2d", 12 | "publishedPluginBundles": [] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /player/gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | 5 | // TypeScript 6 | const ts = require("gulp-typescript"); 7 | const tsProject = ts.createProject("./tsconfig.json"); 8 | const tslint = require("gulp-tslint"); 9 | 10 | gulp.task("typescript", () => { 11 | let failed = false; 12 | const tsResult = tsProject.src() 13 | .pipe(tslint({ formatter: "prose" })) 14 | .pipe(tslint.report({ emitError: true })) 15 | .on("error", (err) => { throw err; }) 16 | .pipe(tsProject()) 17 | .on("error", () => { failed = true; }) 18 | .on("end", () => { if (failed) throw new Error("There were TypeScript errors."); }); 19 | return tsResult.js.pipe(gulp.dest("./")); 20 | }); 21 | 22 | // Browserify 23 | const browserify = require("browserify"); 24 | const source = require("vinyl-source-stream"); 25 | gulp.task("browserify", () => { 26 | const bundler = browserify("./index.js"); 27 | function bundle() { return bundler.bundle().pipe(source("index.js")).pipe(gulp.dest("../public")); } 28 | return bundle(); 29 | }); 30 | 31 | // All 32 | gulp.task("default", gulp.series("typescript", "browserify")); 33 | -------------------------------------------------------------------------------- /player/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as async from "async"; 4 | import * as querystring from "querystring"; 5 | import supFetch from "../../../SupClient/fetch"; 6 | import * as path from "path"; 7 | 8 | const statusElt = document.querySelector(".status") as HTMLDivElement; 9 | let tempFolderPath: string; 10 | 11 | const qs = querystring.parse(window.location.search.slice(1)); 12 | const buildPath = (qs.project != null) ? `/builds/${qs.project}/${qs.build}/` : "./"; 13 | 14 | document.addEventListener("keydown", (event) => { 15 | // F12 16 | if (event.keyCode === 123) SupApp.getCurrentWindow().webContents.toggleDevTools(); 17 | }); 18 | 19 | function start() { 20 | if ((window as any).SupApp == null) { 21 | statusElt.textContent = "Can't run in browser"; 22 | (document.querySelector(".must-use-app") as HTMLDivElement).hidden = false; 23 | return; 24 | } 25 | 26 | document.body.addEventListener("click", (event) => { 27 | if ((event.target as HTMLElement).tagName !== "A") return; 28 | event.preventDefault(); 29 | SupApp.openLink((event.target as HTMLAnchorElement).href); 30 | }); 31 | 32 | if (localStorage["supLove2DPath"] == null) { 33 | showLocateLove(); 34 | } else { 35 | SupApp.tryFileAccess(localStorage["supLove2DPath"], "execute", (err) => { 36 | if (err != null) { showLocateLove(); return; } 37 | downloadGame(); 38 | }); 39 | } 40 | } 41 | 42 | function showLocateLove() { 43 | (document.querySelector(".where-is-love") as HTMLDivElement).hidden = false; 44 | document.querySelector(".where-is-love button").addEventListener("click", onLocateLoveClick); 45 | } 46 | 47 | function onLocateLoveClick(event: Event) { 48 | SupApp.chooseFile("execute", (file) => { 49 | if (file == null) return; 50 | 51 | (document.querySelector(".where-is-love") as HTMLDivElement).hidden = true; 52 | localStorage["supLove2DPath"] = file; 53 | downloadGame(); 54 | }); 55 | } 56 | 57 | function downloadGame() { 58 | statusElt.textContent = "Downloading game..."; 59 | 60 | SupApp.mktmpdir((err, createdFolderPath) => { 61 | if (err != null) { statusElt.textContent = `Could not create temporary folder: ${err.message}`; return; } 62 | 63 | tempFolderPath = createdFolderPath; 64 | 65 | supFetch(`${buildPath}files.json`, "json", (err, filesToDownload) => { 66 | if (err != null) { statusElt.textContent = `Could not load files list: ${err.message}`; return; } 67 | 68 | async.each(filesToDownload, downloadFile, (err) => { 69 | if (err != null) { statusElt.textContent = `Could not download all files: ${err.message}`; return; } 70 | 71 | runGame(); 72 | }); 73 | }); 74 | }); 75 | } 76 | 77 | function downloadFile(filePath: string, callback: (error: Error) => void) { 78 | const inputPath = `${window.location.origin}${buildPath}files/${filePath}`; 79 | const outputPath = path.join(tempFolderPath, filePath); 80 | 81 | SupApp.mkdirp(path.dirname(outputPath), (err) => { 82 | supFetch(inputPath, "arraybuffer", (err, data) => { 83 | if (err != null) { callback(err); return; } 84 | 85 | SupApp.writeFile(outputPath, new Buffer(data), callback); 86 | }); 87 | }); 88 | } 89 | 90 | function runGame() { 91 | statusElt.textContent = "Running LÖVE..."; 92 | 93 | SupApp.spawnChildProcess(localStorage["supLove2DPath"], [ tempFolderPath ], (err, loveProcess) => { 94 | SupApp.getCurrentWindow().hide(); 95 | 96 | let failed = false; 97 | loveProcess.on("error", (err: Error) => { 98 | failed = true; 99 | SupApp.getCurrentWindow().show(); 100 | statusElt.textContent = `Could not start LÖVE: ${err.message}`; 101 | localStorage.removeItem("supLove2DPath"); 102 | }); 103 | loveProcess.on("exit", () => { if (!failed) window.close(); }); 104 | }); 105 | } 106 | 107 | start(); 108 | -------------------------------------------------------------------------------- /player/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "typeRoots": [ "../../../node_modules/@types" ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugins/default/blob/data/BlobAsset.ts: -------------------------------------------------------------------------------- 1 | import * as mkdirp from "mkdirp"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | type UploadCallback = SupCore.Data.Base.ErrorCallback & ((err: string, ack: any, mediaType: string, buffer: Buffer) => void); 6 | 7 | interface BlobAssetPub { 8 | mediaType: string; 9 | buffer: Buffer|ArrayBuffer; 10 | } 11 | 12 | export const defaultExtensions: { [mediaType: string]: string; } = { 13 | "image/gif": "gif", 14 | "image/jpeg": "jpg", 15 | "image/png": "png", 16 | "image/svg+xml": "svg", 17 | 18 | "audio/mp3": "mp3", 19 | "audio/ogg": "ogg", 20 | "audio/opus": "opus", 21 | "audio/wav": "wav", 22 | 23 | "video/mp4": "mp4", 24 | "video/ogg": "ogv", 25 | 26 | "application/font-woff": "woff", 27 | "application/json": "json", 28 | "application/zip": "zip", 29 | 30 | "application/octet-stream": "dat" 31 | }; 32 | 33 | export default class BlobAsset extends SupCore.Data.Base.Asset { 34 | static schema: SupCore.Data.Schema = { 35 | mediaType: { type: "string" }, 36 | buffer: { type: "buffer" } 37 | }; 38 | 39 | pub: BlobAssetPub; 40 | 41 | constructor(id: string, pub: BlobAssetPub, server: ProjectServer) { 42 | super(id, pub, BlobAsset.schema, server); 43 | } 44 | 45 | init(options: any, callback: Function) { 46 | this.pub = { 47 | mediaType: null, 48 | buffer: null 49 | }; 50 | 51 | super.init(options, callback); 52 | } 53 | 54 | load(assetPath: string) { 55 | fs.readFile(path.join(assetPath, "blob.json"), { encoding: "utf8" }, (err, pubJSON) => { 56 | fs.readFile(path.join(assetPath, "blob.dat"), (err, buffer) => { 57 | let pub: BlobAssetPub = JSON.parse(pubJSON); 58 | pub.buffer = buffer; 59 | this._onLoaded(assetPath, pub); 60 | }); 61 | }); 62 | } 63 | 64 | save(assetPath: string, callback: (err: Error) => any) { 65 | let buffer = this.pub.buffer; 66 | delete this.pub.buffer; 67 | let pubJSON = JSON.stringify(this.pub, null, 2); 68 | this.pub.buffer = buffer; 69 | 70 | fs.writeFile(path.join(assetPath, "blob.json"), pubJSON, { encoding: "utf8" }, () => { 71 | if (buffer != null) { 72 | fs.writeFile(path.join(assetPath, "blob.dat"), buffer, callback); 73 | } else { 74 | fs.unlink(path.join(assetPath, "blob.dat"), (err) => { 75 | if (err != null && err.code !== "ENOENT") { callback(err); return; } 76 | callback(null); 77 | }); 78 | } 79 | }); 80 | } 81 | 82 | serverExport(buildPath: string, callback: (err: Error, writtenFiles: string[]) => void) { 83 | this.export(fs.writeFile, mkdirp, buildPath, this.server.data.entries.getPathFromId(this.id), callback); 84 | } 85 | 86 | clientExport(buildPath: string, filePath: string, callback: (err: Error, writtenFiles: string[]) => void) { 87 | this.export(SupApp.writeFile, SupApp.mkdirp, buildPath, filePath, callback); 88 | } 89 | 90 | private export(writeFile: Function, mkdir: Function, buildPath: string, filePath: string, callback: (err: Error, writtenFiles: string[]) => void) { 91 | if (this.pub.buffer == null) { callback (null, []); return; } 92 | 93 | if (filePath.lastIndexOf(".") <= filePath.lastIndexOf("/")) { 94 | // No dots in the name, try adding a default extension 95 | filePath += `.${defaultExtensions[this.pub.mediaType]}`; 96 | } 97 | 98 | const outputPath = `${buildPath}/${filePath}`; 99 | mkdir(path.dirname(outputPath), () => { 100 | const value = this.pub.buffer instanceof ArrayBuffer ? new Buffer(this.pub.buffer) : this.pub.buffer; 101 | writeFile(outputPath, value, (err: Error) => { 102 | if (err != null) callback(err, null); 103 | callback(null, [ filePath ]); 104 | }); 105 | }); 106 | } 107 | 108 | server_upload(client: any, mediaType: string, buffer: Buffer, callback: UploadCallback) { 109 | if (typeof mediaType !== "string" || mediaType.length === 0) { callback("mediaType must be a string"); return; } 110 | if (!(buffer instanceof Buffer)) { callback("buffer must be an ArrayBuffer"); return; } 111 | 112 | this.pub.mediaType = mediaType; 113 | this.pub.buffer = buffer; 114 | 115 | callback(null, null, mediaType, buffer); 116 | this.emit("change"); 117 | } 118 | 119 | client_upload(mediaType: string, buffer: Buffer) { 120 | this.pub.mediaType = mediaType; 121 | this.pub.buffer = buffer; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /plugins/default/blob/data/index.ts: -------------------------------------------------------------------------------- 1 | import BlobAsset from "./BlobAsset"; 2 | 3 | SupCore.system.data.registerAssetClass("blob", BlobAsset); 4 | -------------------------------------------------------------------------------- /plugins/default/blob/editors/blob/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel="stylesheet",href="/styles/reset.css") 5 | link(rel="stylesheet",href="/styles/resizeHandle.css") 6 | link(rel="stylesheet",href="/styles/dialogs.css") 7 | link(rel="stylesheet",href="/styles/properties.css") 8 | link(rel="stylesheet",href="index.css") 9 | 10 | body 11 | .main 12 | .no-preview= t("common:states.loading") 13 | 14 | .sidebar 15 | table.properties 16 | tr 17 | th= t("blobEditor:sidebar.settings.file") 18 | td 19 | button.upload(disabled)= t("common:actions.upload") 20 | form(style="display: none") 21 | input.file-select(type="file") 22 | button.download(disabled)= t("common:actions.download") 23 | tr 24 | th= t("blobEditor:sidebar.settings.mediaType") 25 | td 26 | input.mediaType(type="text",readonly) 27 | tr 28 | th= t("blobEditor:sidebar.settings.filename") 29 | td 30 | input.filename(type="text",readonly) 31 | 32 | script(src="/SupCore.js") 33 | script(src="/SupClient.js") 34 | script(src="../../bundles/data.js") 35 | script(src="index.js") 36 | -------------------------------------------------------------------------------- /plugins/default/blob/editors/blob/index.styl: -------------------------------------------------------------------------------- 1 | body 2 | display flex 3 | 4 | .main 5 | flex 1 6 | display flex 7 | flex-flow column 8 | justify-content center 9 | align-items center 10 | 11 | > * 12 | max-width 100% 13 | max-height 100% 14 | 15 | .no-preview 16 | color #888 17 | text-transform uppercase 18 | 19 | .sidebar 20 | width 250px 21 | min-width 200px 22 | max-width 500px 23 | 24 | display flex 25 | flex-flow column 26 | -------------------------------------------------------------------------------- /plugins/default/blob/editors/blob/index.ts: -------------------------------------------------------------------------------- 1 | import BlobAsset, { defaultExtensions } from "../../data/BlobAsset"; 2 | 3 | import * as ResizeHandle from "resize-handle"; 4 | 5 | // Setup resizable panes 6 | new ResizeHandle(document.querySelector(".sidebar") as HTMLDivElement, "right"); 7 | 8 | let socket: SocketIOClient.Socket; 9 | let projectClient: SupClient.ProjectClient; 10 | let asset: BlobAsset; 11 | let outputFilename: string; 12 | 13 | SupClient.i18n.load([{ root: `${window.location.pathname}/../..`, name: "blobEditor" }], () => { 14 | socket = SupClient.connect(SupClient.query.project); 15 | socket.on("welcome", onWelcome); 16 | socket.on("disconnect", SupClient.onDisconnected); 17 | 18 | // Upload 19 | let fileSelect = document.querySelector("input.file-select"); 20 | fileSelect.addEventListener("change", onFileSelectChange); 21 | document.querySelector("button.upload").addEventListener("click", () => { fileSelect.click(); } ); 22 | document.querySelector("button.download").addEventListener("click", onDownloadBlob); 23 | }); 24 | 25 | function onWelcome() { 26 | projectClient = new SupClient.ProjectClient(socket); 27 | 28 | let subscriber: SupClient.AssetSubscriber = { 29 | onAssetReceived, onAssetEdited, 30 | onAssetTrashed: SupClient.onAssetTrashed 31 | }; 32 | 33 | projectClient.subAsset(SupClient.query.asset, "blob", subscriber); 34 | 35 | let entriesSubscriber: SupClient.EntriesSubscriber = { 36 | onEntriesReceived: () => { setupMediaInfo(); }, 37 | onEntryAdded: () => { /* Ignore */ }, 38 | onEntryMoved: () => { /* Ignore */ }, 39 | onEntryTrashed: () => { /* Ignore */ }, 40 | onSetEntryProperty: (id: string, key: string) => { if (id === SupClient.query.asset && key === "name") setupMediaInfo(); } 41 | }; 42 | projectClient.subEntries(entriesSubscriber); 43 | } 44 | 45 | function onAssetReceived(assetId: string, theAsset: BlobAsset) { 46 | asset = theAsset; 47 | 48 | (document.querySelector("button.upload") as HTMLButtonElement).disabled = false; 49 | (document.querySelector("button.download") as HTMLButtonElement).disabled = false; 50 | setupPreview(); 51 | setupMediaInfo(); 52 | } 53 | 54 | let previewObjectURL: string; 55 | 56 | function setupPreview() { 57 | if (previewObjectURL != null) { 58 | URL.revokeObjectURL(previewObjectURL); 59 | previewObjectURL = null; 60 | } 61 | 62 | let mainElt = document.querySelector(".main") as HTMLDivElement; 63 | mainElt.innerHTML = ""; 64 | 65 | let previewType = (asset.pub.mediaType != null) ? asset.pub.mediaType.split("/")[0] : null; 66 | let typedArray = new Uint8Array(asset.pub.buffer as ArrayBuffer); 67 | let blob = new Blob([ typedArray ], { type: asset.pub.mediaType }); 68 | previewObjectURL = URL.createObjectURL(blob); 69 | 70 | switch (previewType) { 71 | case "image": 72 | case "video": 73 | case "audio": 74 | let playerElt: HTMLImageElement|HTMLVideoElement|HTMLAudioElement; 75 | if (previewType === "image") playerElt = new Image(); 76 | else { 77 | playerElt = document.createElement(previewType) as HTMLAudioElement|HTMLVideoElement; 78 | (playerElt as HTMLAudioElement|HTMLVideoElement).controls = true; 79 | } 80 | 81 | playerElt.src = previewObjectURL; 82 | mainElt.appendChild(playerElt); 83 | break; 84 | 85 | default: 86 | let noPreviewElt = document.createElement("div"); 87 | noPreviewElt.classList.add("no-preview"); 88 | mainElt.appendChild(noPreviewElt); 89 | 90 | if (previewType != null) noPreviewElt.textContent = `No preview available for ${asset.pub.mediaType} (${(asset.pub.buffer as ArrayBuffer).byteLength} bytes).`; 91 | else noPreviewElt.textContent = `No data uploaded yet.`; 92 | break; 93 | } 94 | } 95 | 96 | function setupMediaInfo() { 97 | if (projectClient.entries == null || asset == null) return; 98 | 99 | (document.querySelector("input.mediaType") as HTMLInputElement).value = asset.pub.mediaType; 100 | 101 | let entry = projectClient.entries.byId[SupClient.query.asset]; 102 | outputFilename = entry.name; 103 | 104 | let dotIndex = entry.name.indexOf("."); 105 | if (dotIndex === -1) { 106 | let extension = (dotIndex !== -1) ? entry.name.slice(dotIndex + 1) : defaultExtensions[asset.pub.mediaType]; 107 | if (extension == null) extension = "dat"; 108 | outputFilename += `.${extension}`; 109 | } 110 | 111 | (document.querySelector("input.filename") as HTMLInputElement).value = outputFilename; 112 | } 113 | 114 | function onAssetEdited(assetId: string, command: string, ...args: any[]) { 115 | if (command === "upload") { 116 | setupPreview(); 117 | setupMediaInfo(); 118 | } 119 | } 120 | 121 | function onFileSelectChange(event: any) { 122 | if (event.target.files.length === 0) return; 123 | 124 | let mediaType = event.target.files[0].type; 125 | if (mediaType.length === 0) mediaType = "application/octet-stream"; 126 | 127 | let reader = new FileReader(); 128 | reader.onload = (event) => { 129 | socket.emit("edit:assets", SupClient.query.asset, "upload", mediaType, reader.result, (err: string) => { 130 | if (err != null) { alert(err); return; } 131 | }); 132 | }; 133 | reader.readAsArrayBuffer(event.target.files[0]); 134 | event.target.parentElement.reset(); 135 | } 136 | 137 | function onDownloadBlob() { 138 | if (previewObjectURL == null || outputFilename == null) return; 139 | 140 | function triggerDownload(name: string) { 141 | let anchor = document.createElement("a"); 142 | document.body.appendChild(anchor); 143 | anchor.style.display = "none"; 144 | anchor.href = previewObjectURL; 145 | 146 | // Not yet supported in IE and Safari (http://caniuse.com/#feat=download) 147 | (anchor as any).download = name; 148 | anchor.click(); 149 | document.body.removeChild(anchor); 150 | } 151 | 152 | let options = { 153 | initialValue: outputFilename, 154 | validationLabel: SupClient.i18n.t("common:actions.download") 155 | }; 156 | 157 | if (SupApp != null) { 158 | triggerDownload(options.initialValue); 159 | } else { 160 | new SupClient.Dialogs.PromptDialog(SupClient.i18n.t("blobEditor:sidebar.settings.sound.file.download.prompt"), options, (name) => { 161 | if (name == null) return; 162 | triggerDownload(name); 163 | }); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /plugins/default/blob/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /plugins/default/blob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superpowers-web-sparklinlabs-blob-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "Elisée MAURER ", 6 | "license": "ISC", 7 | "scripts": { 8 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /plugins/default/blob/public/editors/blob/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 65 | 68 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /plugins/default/blob/public/editors/blob/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": false 3 | } -------------------------------------------------------------------------------- /plugins/default/blob/public/locales/en/blobEditor.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": { 3 | "settings": { 4 | "file": "File", 5 | "mediaType": "Media type", 6 | "filename": "Filename", 7 | "download": { 8 | "prompt": "Enter a name for the file." 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugins/default/blob/public/locales/en/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "editors": { 3 | "blob": { 4 | "title": "Media file" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /plugins/default/blob/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "rootDir": "./", 7 | "typeRoots": [ "../../../../../node_modules/@types" ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/default/export/build/Love2dBuildSettings.d.ts: -------------------------------------------------------------------------------- 1 | interface Love2dBuildSettings { 2 | outputFolder: string; 3 | } 4 | -------------------------------------------------------------------------------- /plugins/default/export/build/Love2dBuildSettingsEditor.ts: -------------------------------------------------------------------------------- 1 | import * as TreeView from "dnd-tree-view"; 2 | 3 | let outputFolder: string; 4 | 5 | export default class Loved2BuildSettingsEditor implements SupClient.BuildSettingsEditor { 6 | private outputFolderTextfield: HTMLInputElement; 7 | private outputFolderButton: HTMLButtonElement; 8 | private errorRowElt: HTMLTableRowElement; 9 | private errorInput: HTMLInputElement; 10 | 11 | private table: HTMLTableElement; 12 | 13 | constructor(container: HTMLDivElement, private entries: SupCore.Data.Entries, private entriesTreeView: TreeView) { 14 | const { table, tbody } = SupClient.table.createTable(container); 15 | this.table = table; 16 | table.classList.add("properties"); 17 | table.hidden = true; 18 | 19 | const outputFolderRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("buildSettingsEditors:love2d.outputFolder")); 20 | const inputs = SupClient.html("div", "inputs", { parent: outputFolderRow.valueCell }); 21 | 22 | const value = outputFolder != null ? outputFolder : ""; 23 | this.outputFolderTextfield = SupClient.html("input", { parent: inputs, type: "text", value, readOnly: true, style: { cursor: "pointer" } }) as HTMLInputElement; 24 | this.outputFolderButton = SupClient.html("button", { parent: inputs, textContent: SupClient.i18n.t("common:actions.select") }) as HTMLButtonElement; 25 | 26 | this.outputFolderTextfield.addEventListener("click", (event) => { event.preventDefault(); this.selectOutputfolder(); }); 27 | this.outputFolderButton.addEventListener("click", (event) => { event.preventDefault(); this.selectOutputfolder(); }); 28 | 29 | const errorRow = SupClient.table.appendRow(tbody, "Error"); 30 | this.errorRowElt = errorRow.row; 31 | this.errorRowElt.hidden = true; 32 | this.errorInput = SupClient.html("input", { parent: errorRow.valueCell, type: "text", readOnly: true }) as HTMLInputElement; 33 | } 34 | 35 | setVisible(visible: boolean) { 36 | this.table.hidden = !visible; 37 | } 38 | 39 | getSettings(callback: (settings: Love2dBuildSettings) => void) { 40 | this.ensureOutputFolderValid((outputFolderValid) => { 41 | callback(outputFolderValid ? { outputFolder } : null); 42 | }); 43 | } 44 | 45 | private selectOutputfolder() { 46 | SupApp.chooseFolder((folderPath) => { 47 | if (folderPath == null) return; 48 | 49 | outputFolder = this.outputFolderTextfield.value = folderPath; 50 | this.ensureOutputFolderValid(); 51 | }); 52 | } 53 | 54 | private ensureOutputFolderValid(callback?: (outputFolderValid: boolean) => void) { 55 | if (outputFolder == null) { 56 | this.displayError(SupClient.i18n.t("buildSettingsEditors:love2d.errors.selectDestionationFolder")); 57 | if (callback != null) callback(false); 58 | return; 59 | } 60 | 61 | SupApp.readDir(outputFolder, (err, files) => { 62 | if (err != null) { 63 | this.displayError(SupClient.i18n.t("buildSettingsEditors:love2d.errors.emptyDirectoryCheckFail")); 64 | console.log(err); 65 | if (callback != null) callback(false); 66 | return; 67 | } 68 | 69 | let index = 0; 70 | while (index < files.length) { 71 | const item = files[index]; 72 | if (item[0] === "." || item === "Thumbs.db") files.splice(index, 1); 73 | else index++; 74 | } 75 | 76 | if (files.length > 0) { 77 | this.displayError(SupClient.i18n.t("buildSettingsEditors:love2d.errors.destinationFolderEmpty")); 78 | if (callback != null) callback(false); 79 | } else { 80 | this.errorRowElt.hidden = true; 81 | if (callback != null) callback(true); 82 | } 83 | }); 84 | } 85 | 86 | private displayError(err: string) { 87 | this.errorRowElt.hidden = false; 88 | this.errorInput.value = err; 89 | 90 | (this.errorRowElt as any).animate([ 91 | { transform: "translateX(0)" }, 92 | { transform: "translateX(1.5vh)" }, 93 | { transform: "translateX(0)" } 94 | ], { duration: 100 }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /plugins/default/export/build/buildLove2d.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | 3 | let projectClient: SupClient.ProjectClient; 4 | const subscribersByAssetId: { [assetId: string]: SupClient.AssetSubscriber } = {}; 5 | 6 | let entriesSubscriber: SupClient.EntriesSubscriber; 7 | let entries: SupCore.Data.Entries; 8 | 9 | let settings: Love2dBuildSettings; 10 | let projectWindowId: number; 11 | 12 | const progress = { index: 0, total: 0, errors: 0 }; 13 | const statusElt = document.querySelector(".status"); 14 | const progressElt = document.querySelector("progress") as HTMLProgressElement; 15 | const detailsListElt = document.querySelector(".details ol") as HTMLOListElement; 16 | 17 | interface ClientExportableAsset extends SupCore.Data.Base.Asset { 18 | clientExport: (outputPath: string, filePath: string, callback: (err: Error) => void) => void; 19 | } 20 | 21 | function loadPlugins(callback: Function) { 22 | async.each(SupCore.system.pluginsInfo.list, (pluginName, cb) => { 23 | const pluginPath = `/systems/${SupCore.system.id}/plugins/${pluginName}`; 24 | SupClient.loadScript(`${pluginPath}/bundles/data.js`, cb); 25 | }, () => { callback(); }); 26 | } 27 | 28 | export default function build(socket: SocketIOClient.Socket, theSettings: Love2dBuildSettings, theProjectWindowId: number) { 29 | settings = theSettings; 30 | projectWindowId = theProjectWindowId; 31 | 32 | loadPlugins(() => { 33 | projectClient = new SupClient.ProjectClient(socket); 34 | entriesSubscriber = { onEntriesReceived }; 35 | projectClient.subEntries(entriesSubscriber); 36 | }); 37 | } 38 | 39 | function onEntriesReceived(theEntries: SupCore.Data.Entries) { 40 | entries = theEntries; 41 | projectClient.unsubEntries(entriesSubscriber); 42 | 43 | entries.walk((entry) => { 44 | if (entry.type != null) { 45 | // Only subscribe to assets that can be exported 46 | if (SupCore.system.data.assetClasses[entry.type].prototype.clientExport != null) { 47 | const subscriber = { onAssetReceived }; 48 | subscribersByAssetId[entry.id] = subscriber; 49 | 50 | projectClient.subAsset(entry.id, entry.type, subscriber); 51 | progress.total++; 52 | } 53 | } 54 | }); 55 | } 56 | 57 | function updateProgress() { 58 | progressElt.max = progress.total; 59 | progressElt.value = progress.index; 60 | 61 | if (progress.index < progress.total) { 62 | statusElt.textContent = SupClient.i18n.t("builds:love2d.progress", { path: settings.outputFolder, index: progress.index, total: progress.total }); 63 | } else { 64 | statusElt.textContent = progress.errors > 0 ? 65 | SupClient.i18n.t("builds:love2d.doneWithErrors", { path: settings.outputFolder, total: progress.total, errors: progress.errors }) : 66 | SupClient.i18n.t("builds:love2d.done", { path: settings.outputFolder, total: progress.total }); 67 | 68 | SupApp.sendMessage(projectWindowId, "build-finished"); 69 | } 70 | } 71 | 72 | function onAssetReceived(assetId: string, asset: ClientExportableAsset) { 73 | projectClient.unsubAsset(assetId, subscribersByAssetId[assetId]); 74 | delete subscribersByAssetId[assetId]; 75 | 76 | asset.clientExport(settings.outputFolder, entries.getPathFromId(assetId), (err) => { 77 | if (err != null) { 78 | progress.errors++; 79 | SupClient.html("li", { parent: detailsListElt, textContent: SupClient.i18n.t("builds:love2d.errors.exportFailed", { path: settings.outputFolder }) }); 80 | console.log(err); 81 | } 82 | 83 | progress.index++; 84 | updateProgress(); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /plugins/default/export/build/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import Love2dBuildSettingsEditor from "./Love2dBuildSettingsEditor"; 4 | import buildLove2d from "./buildLove2d"; 5 | 6 | SupClient.registerPlugin("build", "love2d", { 7 | settingsEditor: Love2dBuildSettingsEditor, 8 | build: buildLove2d 9 | }); 10 | -------------------------------------------------------------------------------- /plugins/default/export/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /plugins/default/export/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superpowers-love2d-default-export-plugin", 3 | "description": "Export plugin for Superpowers LÖVE, the collaborative game maker", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/superpowers/superpowers-love2d.git" 9 | }, 10 | "scripts": { 11 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=." 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/default/export/public/locales/en/buildSettingsEditors.json: -------------------------------------------------------------------------------- 1 | { 2 | "love2d": { 3 | "label": "Default", 4 | "description": "Export as LÖVE project", 5 | 6 | "outputFolder": "Output folder", 7 | "errors": { 8 | "selectDestionationFolder": "Select a destination folder.", 9 | "emptyDirectoryCheckFail": "Failed to check if the selected folder is empty.", 10 | "destinationFolderEmpty": "The destination folder must be empty." 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/default/export/public/locales/en/builds.json: -------------------------------------------------------------------------------- 1 | { 2 | "love2d": { 3 | "title": "LÖVE Export", 4 | "progress": "Exporting to ${path}... (${index}/${total})", 5 | "doneWithErrors": "Encountered ${errors} errors while exporting ${total} elements to ${path}.", 6 | "done": "Exported ${total} elements to ${path}.", 7 | 8 | "errors": { 9 | "exportFailed": "Failed to export ${path}." 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugins/default/export/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "rootDir": "./", 7 | "typeRoots": [ "../../../../../node_modules/@types" ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugins/default/lua/data/LuaAsset.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as OT from "operational-transform"; 4 | import * as mkdirp from "mkdirp"; 5 | import * as path from "path"; 6 | 7 | import * as dummy_fs from "fs"; 8 | // Since we're doing weird things to the fs module, 9 | // the code won't browserify properly with brfs 10 | // so we'll only require them on the server-side 11 | let serverRequire = require; 12 | 13 | let fs: typeof dummy_fs; 14 | if ((global as any).window == null) { 15 | fs = serverRequire("fs"); 16 | } 17 | 18 | type EditTextCallback = SupCore.Data.Base.ErrorCallback & ((err: string, ack: any, operationData: OperationData, revisionIndex: number) => void); 19 | type ApplyDraftChangedCallback = SupCore.Data.Base.ErrorCallback; 20 | 21 | interface LuaAssetPub { 22 | text: string; 23 | draft: string; 24 | revisionId: number; 25 | } 26 | 27 | export default class LuaAsset extends SupCore.Data.Base.Asset { 28 | static schema: SupCore.Data.Schema = { 29 | text: { type: "string" }, 30 | draft: { type: "string" }, 31 | revisionId: { type: "integer" } 32 | }; 33 | 34 | pub: LuaAssetPub; 35 | document: OT.Document; 36 | hasDraft: boolean; 37 | 38 | constructor(id: string, pub: LuaAssetPub, server: ProjectServer) { 39 | super(id, pub, LuaAsset.schema, server); 40 | } 41 | 42 | init(options: any, callback: Function) { 43 | this.pub = { 44 | text: "", 45 | draft: "", 46 | revisionId: 0 47 | }; 48 | 49 | super.init(options, callback); 50 | } 51 | 52 | setup() { 53 | this.document = new OT.Document(this.pub.draft, this.pub.revisionId); 54 | this.hasDraft = this.pub.text !== this.pub.draft; 55 | } 56 | 57 | restore() { 58 | if (this.hasDraft) this.emit("setBadge", "draft", "info"); 59 | } 60 | 61 | load(assetPath: string) { 62 | let pub: LuaAssetPub; 63 | fs.readFile(path.join(assetPath, "script.lua"), { encoding: "utf8" }, (err, text) => { 64 | fs.readFile(path.join(assetPath, "draft.lua"), { encoding: "utf8" }, (err, draft) => { 65 | pub = { revisionId: 0, text, draft: (draft != null) ? draft : text }; 66 | 67 | pub.draft = pub.draft.replace(/\r\n/g, "\n"); 68 | pub.text = pub.text.replace(/\r\n/g, "\n"); 69 | 70 | this._onLoaded(assetPath, pub); 71 | }); 72 | }); 73 | } 74 | 75 | save(assetPath: string, callback: (err: Error) => any) { 76 | fs.writeFile(path.join(assetPath, "script.lua"), this.pub.text, { encoding: "utf8" }, (err) => { 77 | if (err != null) { callback(err); return; } 78 | 79 | if (this.hasDraft) { 80 | fs.writeFile(path.join(assetPath, "draft.lua"), this.pub.draft, { encoding: "utf8" }, callback); 81 | } else { 82 | fs.unlink(path.join(assetPath, "draft.lua"), (err) => { 83 | if (err != null && err.code !== "ENOENT") { callback(err); return; } 84 | callback(null); 85 | }); 86 | } 87 | }); 88 | } 89 | 90 | serverExport(buildPath: string, callback: (err: Error, writtenFiles: string[]) => void) { 91 | this.export(fs.writeFile, mkdirp, buildPath, this.server.data.entries.getPathFromId(this.id), callback); 92 | } 93 | 94 | clientExport(buildPath: string, filePath: string, callback: (err: Error, writtenFiles: string[]) => void) { 95 | this.export(SupApp.writeFile, SupApp.mkdirp, buildPath, filePath, callback); 96 | } 97 | 98 | private export(writeFile: Function, mkdir: Function, buildPath: string, filePath: string, callback: (err: Error, writtenFiles: string[]) => void) { 99 | if (filePath.lastIndexOf(".lua") === filePath.length - 4) filePath = filePath.slice(0, -4); 100 | filePath += ".lua"; 101 | const outputPath = `${buildPath}/${filePath}`; 102 | 103 | const text = this.pub.text; 104 | mkdir(path.dirname(outputPath), () => { 105 | writeFile(outputPath, text, (err: Error) => { 106 | if (err != null) { callback(err, null); return; } 107 | callback(null, [ filePath ]); 108 | }); 109 | }); 110 | } 111 | 112 | server_editText(client: any, operationData: OperationData, revisionIndex: number, callback: EditTextCallback) { 113 | if (operationData.userId !== client.id) { callback("Invalid client id"); return; } 114 | 115 | let operation = new OT.TextOperation(); 116 | if (!operation.deserialize(operationData)) { callback("Invalid operation data"); return; } 117 | 118 | try { operation = this.document.apply(operation, revisionIndex); } 119 | catch (err) { callback("Operation can't be applied"); return; } 120 | 121 | this.pub.draft = this.document.text; 122 | this.pub.revisionId++; 123 | 124 | callback(null, null, operation.serialize(), this.document.getRevisionId() - 1); 125 | 126 | if (!this.hasDraft) { 127 | this.hasDraft = true; 128 | this.emit("setBadge", "draft", "info"); 129 | } 130 | this.emit("change"); 131 | } 132 | 133 | client_editText(operationData: OperationData, revisionIndex: number) { 134 | let operation = new OT.TextOperation(); 135 | operation.deserialize(operationData); 136 | this.document.apply(operation, revisionIndex); 137 | this.pub.draft = this.document.text; 138 | this.pub.revisionId++; 139 | } 140 | 141 | server_applyDraftChanges(client: any, callback: ApplyDraftChangedCallback) { 142 | this.pub.text = this.pub.draft; 143 | 144 | callback(null); 145 | 146 | if (this.hasDraft) { 147 | this.hasDraft = false; 148 | this.emit("clearBadge", "draft"); 149 | } 150 | 151 | this.emit("change"); 152 | } 153 | 154 | client_applyDraftChanges() { this.pub.text = this.pub.draft; } 155 | } 156 | -------------------------------------------------------------------------------- /plugins/default/lua/data/index.ts: -------------------------------------------------------------------------------- 1 | import LuaAsset from "./LuaAsset"; 2 | 3 | SupCore.system.data.registerAssetClass("lua", LuaAsset); 4 | -------------------------------------------------------------------------------- /plugins/default/lua/editors/lua/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel="stylesheet",href="/styles/reset.css") 5 | // link(rel="stylesheet",href="/styles/resizeHandle.css") 6 | link(rel="stylesheet",href="/styles/dialogs.css") 7 | link(rel="stylesheet",href="../../../../common/textEditorWidget/widget.css") 8 | link(rel="stylesheet",href="index.css") 9 | 10 | body 11 | .text-editor-container 12 | textarea(disabled).text-editor 13 | .status-pane 14 | .header 15 | .draft 16 | button.save(title= t("scriptEditor:applyChanges"),data-hotkey="control+S")= t("common:actions.applyChanges") 17 | span.label= t("scriptEditor:draft") 18 | span — 19 | .info= t("common:states.loading") 20 | 21 | script(src="/SupCore.js") 22 | script(src="/SupClient.js") 23 | script(src="../../bundles/data.js") 24 | script(src="../../../../common/textEditorWidget/bundles/data.js") 25 | script(src="../../../../common/textEditorWidget/widget.js") 26 | script(src="../../../../common/textEditorWidget/codemirror/mode/javascript/javascript.js") 27 | script(src="../../../../common/textEditorWidget/codemirror/mode/lua/lua.js") 28 | script(src="index.js") 29 | -------------------------------------------------------------------------------- /plugins/default/lua/editors/lua/index.styl: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | } 6 | 7 | .text-editor-container { 8 | min-height: 100px; 9 | } 10 | 11 | .CodeMirror .CodeMirror-overwrite .CodeMirror-cursor { 12 | border-left: 8px solid rgba(86, 86, 232, 0.5); 13 | } 14 | 15 | .status-pane { 16 | border-top: 1px solid #888; 17 | display: flex; 18 | flex-flow: column; 19 | 20 | .header { 21 | background-color: #444; 22 | color: #fff; 23 | display: flex; 24 | align-items: center; 25 | 26 | &.has-draft { background-color: #37d; } 27 | &.has-draft .draft { display: block; } 28 | 29 | .draft { 30 | display: none; 31 | padding-left: 0.25em; 32 | color: rgba(255,255,255,0.5); 33 | 34 | .label { padding-left: 0.5em; } 35 | } 36 | 37 | .info { 38 | flex: 1; 39 | padding: 0.5em; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /plugins/default/lua/editors/lua/index.ts: -------------------------------------------------------------------------------- 1 | import LuaAsset from "../../data/LuaAsset"; 2 | 3 | let socket: SocketIOClient.Socket; 4 | let projectClient: SupClient.ProjectClient; 5 | let editor: TextEditorWidget; 6 | let asset: LuaAsset; 7 | 8 | SupClient.i18n.load([{ root: `${window.location.pathname}/../..`, name: "scriptEditor" }], () => { 9 | socket = SupClient.connect(SupClient.query.project); 10 | socket.on("welcome", onWelcome); 11 | socket.on("disconnect", SupClient.onDisconnected); 12 | }); 13 | 14 | const statusPaneHeader = document.querySelector(".status-pane .header"); 15 | const statusPaneSaveButton = statusPaneHeader.querySelector(".save") as HTMLButtonElement; 16 | const statusPaneInfo = statusPaneHeader.querySelector(".info"); 17 | 18 | statusPaneSaveButton.addEventListener("click", (event: MouseEvent) => { 19 | event.preventDefault(); 20 | event.stopPropagation(); 21 | applyDraftChanges(); 22 | }); 23 | 24 | function onWelcome(clientId: string) { 25 | projectClient = new SupClient.ProjectClient(socket); 26 | setupEditor(clientId); 27 | 28 | let subscriber: SupClient.AssetSubscriber = { 29 | onAssetReceived, onAssetEdited, 30 | onAssetTrashed: SupClient.onAssetTrashed 31 | }; 32 | 33 | projectClient.subAsset(SupClient.query.asset, "lua", subscriber); 34 | } 35 | 36 | function onAssetReceived(assetId: string, theAsset: LuaAsset) { 37 | asset = theAsset; 38 | editor.setText(asset.pub.draft); 39 | statusPaneInfo.textContent = SupClient.i18n.t("scriptEditor:ready"); 40 | } 41 | 42 | function onAssetEdited(assetId: string, command: string, ...args: any[]) { 43 | if (command === "editText") { 44 | statusPaneHeader.classList.add("has-draft"); 45 | editor.receiveEditText(args[0]); 46 | } else if (command === "applyDraftChanges") { 47 | statusPaneHeader.classList.remove("has-draft"); 48 | } 49 | } 50 | 51 | function setupEditor(clientId: string) { 52 | let textArea = document.querySelector(".text-editor"); 53 | editor = new TextEditorWidget(projectClient, clientId, textArea, { 54 | mode: "text/x-lua", 55 | extraKeys: { 56 | "Ctrl-S": () => { applyDraftChanges(); }, 57 | "Cmd-S": () => { applyDraftChanges(); }, 58 | }, 59 | editCallback: onEditText, 60 | sendOperationCallback: onSendOperation 61 | }); 62 | } 63 | 64 | function onEditText(text: string, origin: string) { /* Ignore */ } 65 | 66 | function onSendOperation(operation: OperationData) { 67 | socket.emit("edit:assets", SupClient.query.asset, "editText", operation, asset.document.getRevisionId(), (err: string) => { 68 | if (err != null) { alert(err); SupClient.onDisconnected(); } 69 | }); 70 | } 71 | 72 | function applyDraftChanges() { 73 | statusPaneSaveButton.disabled = true; 74 | statusPaneSaveButton.textContent = SupClient.i18n.t("common:states.saving"); 75 | 76 | socket.emit("edit:assets", SupClient.query.asset, "applyDraftChanges", (err: string) => { 77 | if (err != null) { alert(err); SupClient.onDisconnected(); return; } 78 | 79 | statusPaneSaveButton.disabled = false; 80 | statusPaneSaveButton.textContent = SupClient.i18n.t("common:actions.applyChanges"); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /plugins/default/lua/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /plugins/default/lua/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superpowers-love2d-sparklinlabs-lua-plugin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "operational-transform": { 8 | "version": "0.2.3", 9 | "resolved": "https://registry.npmjs.org/operational-transform/-/operational-transform-0.2.3.tgz", 10 | "integrity": "sha1-zzJ3QxK0u5pGR465Sfpqyq3V1Ag=" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugins/default/lua/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superpowers-love2d-sparklinlabs-lua-plugin", 3 | "description": "Lua plugin for Superpowers LÖVE, the collaborative game maker", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "scripts": { 7 | "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=." 8 | }, 9 | "dependencies": { 10 | "operational-transform": "^0.2.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugins/default/lua/public/editors/lua/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 63 | 70 | 77 | 78 | -------------------------------------------------------------------------------- /plugins/default/lua/public/editors/lua/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "revision": false 3 | } -------------------------------------------------------------------------------- /plugins/default/lua/public/locales/en/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "editors": { 3 | "lua": { 4 | "title": "Lua script" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /plugins/default/lua/public/locales/en/scriptEditor.json: -------------------------------------------------------------------------------- 1 | { 2 | "applyChanges": "Apply changes", 3 | "draft": "Draft", 4 | 5 | "ready": "Ready" 6 | } 7 | -------------------------------------------------------------------------------- /plugins/default/lua/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "rootDir": "./", 7 | "typeRoots": [ "../../../../../node_modules/@types" ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Superpowers LÖVE 6 | 26 | 27 | 28 |
Looking for LÖVE...
29 | 30 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/locales/de/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "LÖVE", 3 | "description": "LÖVE lässt Sie 2D Spiele in Lua erstellen." 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/en/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "LÖVE", 3 | "description": "LÖVE lets you make 2D games in Lua." 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/it/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "LÖVE", 3 | "description": "LÖVE ti permette di creare giochi in 2D con Lua." 4 | } 5 | -------------------------------------------------------------------------------- /server/gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | 5 | // TypeScript 6 | const ts = require("gulp-typescript"); 7 | const tsProject = ts.createProject("./tsconfig.json"); 8 | const tslint = require("gulp-tslint"); 9 | 10 | gulp.task("typescript", () => { 11 | let failed = false; 12 | const tsResult = tsProject.src() 13 | .pipe(tslint({ formatter: "prose" })) 14 | .pipe(tslint.report({ emitError: true })) 15 | .on("error", (err) => { throw err; }) 16 | .pipe(tsProject()) 17 | .on("error", () => { failed = true; }) 18 | .on("end", () => { if (failed) throw new Error("There were TypeScript errors."); }); 19 | return tsResult.js.pipe(gulp.dest("./")); 20 | }); 21 | 22 | // All 23 | gulp.task("default", gulp.parallel("typescript")); 24 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as fs from "fs"; 4 | import * as async from "async"; 5 | 6 | interface ServerExportableAsset extends SupCore.Data.Base.Asset { 7 | serverExport: (folderPath: string, callback: (err: Error, writtenFiles: string[]) => void) => void; 8 | } 9 | 10 | SupCore.system.serverBuild = (server: ProjectServer, buildPath: string, callback: (err: string) => void) => { 11 | fs.mkdirSync(`${buildPath}/files`); 12 | 13 | const assetIdsToExport: string[] = []; 14 | server.data.entries.walk((entry: SupCore.Data.EntryNode, parent: SupCore.Data.EntryNode) => { 15 | if (entry.type != null && server.system.data.assetClasses[entry.type].prototype.serverExport != null) assetIdsToExport.push(entry.id); 16 | }); 17 | 18 | let files: string[] = []; 19 | 20 | async.each(assetIdsToExport, (assetId, cb) => { 21 | server.data.assets.acquire(assetId, null, (err: Error, asset: ServerExportableAsset) => { 22 | asset.serverExport(`${buildPath}/files`, (err, writtenFiles) => { 23 | server.data.assets.release(assetId, null); 24 | 25 | files = files.concat(writtenFiles); 26 | cb(); 27 | }); 28 | }); 29 | }, (err) => { 30 | if (err != null) { callback("Could not export all assets"); return; } 31 | 32 | const json = JSON.stringify(files, null, 2); 33 | fs.writeFile(`${buildPath}/files.json`, json, { encoding: "utf8" }, (err) => { 34 | if (err != null) { callback("Could not save files.json"); return; } 35 | 36 | callback(null); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "typeRoots": [ "../../../node_modules/@types" ] 7 | } 8 | } 9 | --------------------------------------------------------------------------------