var fs = require('fs'); // Constants var con = { allowable_connections: [ ['dialogue.Text', 'dialogue.Text'], ['dialogue.Text', 'dialogue.Node'], ['dialogue.Text', 'dialogue.Choice'], ['dialogue.Text', 'dialogue.Set'], ['dialogue.Text', 'dialogue.Branch'], ['dialogue.Text', 'dialogue.Function'], ['dialogue.Text', 'dialogue.Tree'], ['dialogue.Text', 'dialogue.GoToLabel'], ['dialogue.Node', 'dialogue.Text'], ['dialogue.Node', 'dialogue.Node'], ['dialogue.Node', 'dialogue.Choice'], ['dialogue.Node', 'dialogue.Set'], ['dialogue.Node', 'dialogue.Branch'], ['dialogue.Node', 'dialogue.Function'], ['dialogue.Node', 'dialogue.Tree'], ['dialogue.Node', 'dialogue.GoToLabel'], ['dialogue.Tree', 'dialogue.Node'], ['dialogue.Tree', 'dialogue.Text'], ['dialogue.Tree', 'dialogue.Choice'], ['dialogue.Tree', 'dialogue.Set'], ['dialogue.Tree', 'dialogue.Branch'], ['dialogue.Tree', 'dialogue.Function'], ['dialogue.Tree', 'dialogue.Tree'], ['dialogue.Tree', 'dialogue.GoToLabel'], ['dialogue.StartNode', 'dialogue.Text'], ['dialogue.StartNode', 'dialogue.Node'], ['dialogue.StartNode', 'dialogue.Choice'], ['dialogue.StartNode', 'dialogue.Set'], ['dialogue.StartNode', 'dialogue.Branch'], ['dialogue.StartNode', 'dialogue.Function'], ['dialogue.StartNode', 'dialogue.Tree'], ['dialogue.StartNode', 'dialogue.GoToLabel'], ['dialogue.Choice', 'dialogue.Text'], ['dialogue.Choice', 'dialogue.Node'], ['dialogue.Choice', 'dialogue.Set'], ['dialogue.Choice', 'dialogue.Branch'], ['dialogue.Choice', 'dialogue.Function'], ['dialogue.Choice', 'dialogue.Tree'], ['dialogue.Choice', 'dialogue.GoToLabel'], ['dialogue.Set', 'dialogue.Text'], ['dialogue.Set', 'dialogue.Node'], ['dialogue.Set', 'dialogue.Set'], ['dialogue.Set', 'dialogue.Branch'], ['dialogue.Set', 'dialogue.Function'], ['dialogue.Set', 'dialogue.Tree'], ['dialogue.Set', 'dialogue.GoToLabel'], ['dialogue.Function', 'dialogue.Text'], ['dialogue.Function', 'dialogue.Node'], ['dialogue.Function', 'dialogue.Set'], ['dialogue.Function', 'dialogue.Branch'], ['dialogue.Function', 'dialogue.Function'], ['dialogue.Function', 'dialogue.Tree'], ['dialogue.Function', 'dialogue.GoToLabel'], ['dialogue.Branch', 'dialogue.Text'], ['dialogue.Branch', 'dialogue.Node'], ['dialogue.Branch', 'dialogue.Set'], ['dialogue.Branch', 'dialogue.Branch'], ['dialogue.Branch', 'dialogue.Function'], ['dialogue.Branch', 'dialogue.Tree'], ['dialogue.Branch', 'dialogue.GoToLabel'], ], default_link: new joint.dia.Link( { attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', }, '.link-tools .tool-remove circle, .marker-vertex': { r: 8 }, }, }), }; con.default_link.set('smooth', true); // State var state = { graph: new joint.dia.Graph(), paper: null, filepath: null, panning: false, mouse_position: { x: 0, y: 0 }, context_position: { x: 0, y: 0 }, menu: null, }; // Models joint.shapes.dialogue = {}; joint.shapes.dialogue.Base = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Base', size: { width: 100, height: 64 }, name: '', attrs: { rect: { stroke: 'none', 'fill-opacity': 0 }, text: { display: 'none' }, }, }, joint.shapes.devs.Model.prototype.defaults ), }); joint.shapes.dialogue.BaseView = joint.shapes.devs.ModelView.extend( { template: [ '
', '', '', '', '
', ].join(''), initialize: function() { _.bindAll(this, 'updateBox'); joint.shapes.devs.ModelView.prototype.initialize.apply(this, arguments); this.$box = $(_.template(this.template)()); // Prevent paper from handling pointerdown. this.$box.find('textarea, input').on('mousedown click', function(evt) { evt.stopPropagation(); }); // This is an example of reacting on the input change and storing the input data in the cell model. this.$box.find('textarea.name, input.name').on('change', _.bind(function(evt) { this.model.set('name', $(evt.target).val()); }, this)); this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model)); // Update the box position whenever the underlying model changes. this.model.on('change', this.updateBox, this); // Remove the box when the model gets removed from the graph. this.model.on('remove', this.removeBox, this); this.updateBox(); }, render: function() { joint.shapes.devs.ModelView.prototype.render.apply(this, arguments); this.paper.$el.prepend(this.$box); this.updateBox(); return this; }, updateBox: function() { // Set the position and dimension of the box so that it covers the JointJS element. var bbox = this.model.getBBox(); // Example of updating the HTML with a data stored in the cell model. var nameField = this.$box.find('textarea.name, input.name'); if (!nameField.is(':focus')) nameField.val(this.model.get('name')); var label = this.$box.find('.label'); var type = this.model.get('type').slice('dialogue.'.length); label.text(type); label.attr('class', 'label ' + type); this.$box[0].classList.add(type); this.$box.css({ width: bbox.width, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' }); }, removeBox: function(evt) { this.$box.remove(); }, }); joint.shapes.dialogue.Node = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Node', size: { width: 100, height: 65, }, inPorts: ['input'], outPorts: ['output'], attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.NodeView = joint.shapes.dialogue.BaseView; joint.shapes.dialogue.Tree = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Tree', size: { width: 150, height: 65, }, inPorts: ['input'], outPorts: ['output'], attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.TreeView = joint.shapes.dialogue.BaseView; joint.shapes.dialogue.Text = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Text', size: { width: 350 }, inPorts: ['input'], outPorts: ['output'], attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.TextView = joint.shapes.dialogue.BaseView; joint.shapes.dialogue.Function = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Function', size: { width: 200, height: 65, }, inPorts: ['input'], outPorts: ['output'], attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.FunctionView = joint.shapes.dialogue.BaseView; joint.shapes.dialogue.GoToLabel = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.GoToLabel', size: { width: 125, height: 65, }, inPorts: ['input'], outPorts: ['output'], attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.GoToLabelView = joint.shapes.dialogue.BaseView; joint.shapes.dialogue.Branch = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Branch', size: { width: 150, height: 95, }, inPorts: ['input'], outPorts: ['output0'], values: [], }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.BranchView = joint.shapes.dialogue.BaseView.extend( { template: [ '
', '', '', '', '', '', '', '
', ].join(''), initialize: function() { joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments); this.$box.find('.add').on('click', _.bind(this.addPort, this)); this.$box.find('.remove').on('click', _.bind(this.removePort, this)); }, removePort: function() { if (this.model.get('outPorts').length > 1) { var outPorts = this.model.get('outPorts').slice(0); outPorts.pop(); this.model.set('outPorts', outPorts); var values = this.model.get('values').slice(0); values.pop(); this.model.set('values', values); this.updateSize(); } }, addPort: function() { var outPorts = this.model.get('outPorts').slice(0); outPorts.push('output' + outPorts.length.toString()); this.model.set('outPorts', outPorts); var values = this.model.get('values').slice(0); values.push(null); this.model.set('values', values); this.updateSize(); }, updateBox: function() { joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments); var values = this.model.get('values'); var valueFields = this.$box.find('input.value'); // Add value fields if necessary for (var i = valueFields.length; i < values.length; i++) { // Prevent paper from handling pointerdown. var $field = $(''); $field.attr('placeholder', 'Value ' + (i + 1).toString()); $field.attr('index', i); this.$box.append($field); $field.on('mousedown click', function(evt) { evt.stopPropagation(); }); // This is an example of reacting on the input change and storing the input data in the cell model. $field.on('change', _.bind(function(evt) { var values = this.model.get('values').slice(0); values[$(evt.target).attr('index')] = $(evt.target).val(); this.model.set('values', values); }, this)); } // Remove value fields if necessary for (var i = values.length; i < valueFields.length; i++) $(valueFields[i]).remove(); // Update value fields valueFields = this.$box.find('input.value'); for (var i = 0; i < valueFields.length; i++) { var field = $(valueFields[i]); if (!field.is(':focus')) field.val(values[i]); } }, updateSize: function() { var textField = this.$box.find('input.name'); var height = textField.outerHeight(true); this.model.set('size', { width: 150, height: 95 + Math.max(0, (this.model.get('outPorts').length - 1) * height) }); }, }); joint.shapes.dialogue.StartNode = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.StartNode', inPorts: [], outPorts: ['output'], size: { width: 100, height: 30, }, value: 'DialogueNode.START', attrs: { '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], } }, }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.StartNodeView = joint.shapes.dialogue.BaseView.extend( { template: [ '
', '', '', '
', ].join(''), initialize: function() { joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments); //this.$box.find('input.value').on('change', _.bind(function(evt) //{ // this.model.set('value', $(evt.target).val()); //}, this)); }, updateBox: function() { joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments); //var field = this.$box.find('input.value'); //if (!field.is(':focus')) // field.val(this.model.get('value')); }, }); joint.shapes.dialogue.Choice = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Choice', inPorts: ['input'], outPorts: ['output'], size: { width: 175, height: 95, }, value: '', }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.ChoiceView = joint.shapes.dialogue.BaseView.extend( { template: [ '
', '', '', '', '', '
', ].join(''), initialize: function() { joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments); this.$box.find('input.value').on('change', _.bind(function(evt) { this.model.set('value', $(evt.target).val()); }, this)); }, updateBox: function() { joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments); var field = this.$box.find('input.value'); if (!field.is(':focus')) field.val(this.model.get('value')); }, }); joint.shapes.dialogue.Set = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.Set', inPorts: ['input'], outPorts: ['output'], size: { width: 125, height: 90, }, value: '', }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.SetView = joint.shapes.dialogue.BaseView.extend( { template: [ '
', '', '', '', '', '
', ].join(''), initialize: function() { joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments); this.$box.find('input.value').on('change', _.bind(function(evt) { this.model.set('value', $(evt.target).val()); }, this)); }, updateBox: function() { joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments); var field = this.$box.find('input.value'); if (!field.is(':focus')) field.val(this.model.get('value')); }, }); /** * * AI Hook * */ joint.shapes.dialogue.AIHook = joint.shapes.devs.Model.extend( { defaults: joint.util.deepSupplement ( { type: 'dialogue.AIHook', inPorts: [], outPorts: [], size: { width: 300, height: 124, }, value: '', }, joint.shapes.dialogue.Base.prototype.defaults ), }); joint.shapes.dialogue.AIHookView = joint.shapes.dialogue.BaseView.extend( { template: [ '
', '', '', '', '', '', '
', ].join(''), initialize: function() { joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments); this.$box.find('input.conditions').on('change', _.bind(function(evt) { this.model.set('conditions', $(evt.target).val()); }, this)); this.$box.find('input.hooktype').on('change', _.bind(function(evt) { this.model.set('hooktype', $(evt.target).val()); }, this)); this.$box.find('input.identifier').on('change', _.bind(function(evt) { this.model.set('identifier', $(evt.target).val()); }, this)); }, updateBox: function() { joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments); var field = this.$box.find('input.hooktype'); if (!field.is(':focus')) field.val(this.model.get('hooktype')); var field = this.$box.find('input.conditions'); if (!field.is(':focus')) field.val(this.model.get('conditions')); var field = this.$box.find('input.identifier'); if (!field.is(':focus')) field.val(this.model.get('identifier')); }, }); // Functions var func = {}; func.validate_connection = function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { // Prevent loop linking if (magnetS === magnetT) return false; if (cellViewS === cellViewT) return false; if ($(magnetT).parents('.outPorts').length > 0) // Can't connect to an output port return false; var sourceType = cellViewS.model.attributes.type; var targetType = cellViewT.model.attributes.type; var valid = false; for (var i = 0; i < con.allowable_connections.length; i++) { var rule = con.allowable_connections[i]; if (sourceType === rule[0] && targetType === rule[1]) { valid = true; break; } } if (!valid) return false; var links = state.graph.getConnectedLinks(cellViewS.model); for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.attributes.source.id === cellViewS.model.id && link.attributes.source.port === magnetS.attributes.port.nodeValue && link.attributes.target.id) { var targetCell = state.graph.getCell(link.attributes.target.id); if (targetCell.attributes.type !== targetType) return false; // We can only connect to multiple targets of the same type if (targetCell === cellViewT.model) return false; // Already connected } } return true; }; func.validate_magnet = function(cellView, magnet) { if ($(magnet).parents('.outPorts').length === 0) return false; // we only want output ports // If unlimited connections attribute is null, we can only ever connect to one object // If it is not null, it is an array of type strings which are allowed to have unlimited connections var unlimitedConnections = magnet.getAttribute('unlimitedConnections'); var links = state.graph.getConnectedLinks(cellView.model); for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.attributes.source.id === cellView.model.id && link.attributes.source.port === magnet.attributes.port.nodeValue) { // This port already has a connection if (unlimitedConnections && link.attributes.target.id) { var targetCell = state.graph.getCell(link.attributes.target.id); if (unlimitedConnections.indexOf(targetCell.attributes.type) !== -1) return true; // It's okay because this target type has unlimited connections } return false; } } return true; }; func.optimized_data = function() { var cells = state.graph.toJSON().cells; var nodesByID = {}; var cellsByID = {}; var nodes = []; for (var i = 0; i < cells.length; i++) { var cell = cells[i]; if (cell.type != 'link') { var node = { type: cell.type.slice('dialogue.'.length), id: cell.id, }; if (node.type === 'Branch') { node.variable = cell.name; node.branches = {}; for (var j = 0; j < cell.values.length; j++) { var branch = cell.values[j]; node.branches[branch] = null; } } else if (node.type === 'Set') { node.variable = cell.name; node.value = cell.value; node.next = null; } else if (node.type === 'AIHook') { node.hooktype = cell.hooktype; node.identifier = cell.identifier; node.conditions = cell.conditions; node.next = null; } else { node.name = cell.name; node.next = null; } if (node.type === "Choice") { node.value = cell.value; } nodes.push(node); nodesByID[cell.id] = node; cellsByID[cell.id] = cell; } } for (var i = 0; i < cells.length; i++) { var cell = cells[i]; if (cell.type === 'link') { var source = nodesByID[cell.source.id]; var target = cell.target ? nodesByID[cell.target.id] : null; if (source) { if (source.type === 'Branch') { var portNumber = parseInt(cell.source.port.slice('output'.length)); var value; if (portNumber === 0) value = '_default'; else { var sourceCell = cellsByID[source.id]; value = sourceCell.values[portNumber - 1]; } source.branches[value] = target ? target.id : null; } else if ((source.type === 'Text' || source.type === 'Node' || source.type === 'StartNode') && target && target.type === 'Choice') { if (!source.choices) { source.choices = []; delete source.next; } source.choices.push(target.id); } else source.next = target ? target.id : null; } } } return nodes; }; // Menu actions func.flash = function(text) { var $flash = $('#flash'); $flash.text(text); $flash.stop(true, true); $flash.show(); $flash.css('opacity', 1.0); $flash.fadeOut({ duration: 1500 }); }; func.apply_fields = function() { $('input[type=text], select').blur(); }; func.save = function() { func.apply_fields(); if (!state.filepath) func.show_save_dialog(); else func.do_save(); }; func.optimized_filename = function(f) { return f.substring(0, f.length - 2) + 'dlz'; }; func.perfect_typescript = function(f) { return f.substring(0, f.length - 2) + 'dl.ts'; }; func.just_name = function (f) { return f.substring(0, f.length - 3); }; func.do_save = function() { if (state.filepath) { fs.writeFileSync(state.filepath, JSON.stringify(state.graph), 'utf8'); //fs.writeFileSync(func.optimized_filename(state.filepath), JSON.stringify(func.optimized_data()), 'utf8'); let name = func.just_name(func.filename_from_filepath(state.filepath)); let cleants = "// File created automatically by custom Dialogger on " + new Date().toLocaleString() + "\n"; cleants += "// Do not tamper with this file.\n// It will be replaced automatically by Dialogger and all changes will be lost.\n// Instead change " + name + ".dl."; cleants += "\nmodule DialogueTrees {\n export let " + name + " = (function () {\n"; cleants += func.ts_data(func.optimized_data(), name).replace(/^/gm, ' ') + "\n return tree;\n })();\n}"; fs.writeFileSync(func.perfect_typescript(state.filepath), cleants, 'utf8'); func.flash('Saved ' + state.filepath); } }; func.filename_from_filepath = function(f) { return f.replace(/^.*[\\\/]/, ''); }; func.show_open_dialog = function() { $('#file_open').click(); }; func.show_save_dialog = function() { $('#file_save').click(); }; func.add_node = function(constructor) { return function() { var container = $('#container')[0]; var element = new constructor( { position: { x: state.context_position.x + container.scrollLeft, y: state.context_position.y + container.scrollTop }, }); state.graph.addCells([element]); }; }; func.clear = function() { state.graph.clear(); state.filepath = null; document.title = 'Dialogger'; }; func.handle_open_files = function(files) { state.filepath = files[0].path; var data = fs.readFileSync(state.filepath); document.title = func.filename_from_filepath(state.filepath); state.graph.clear(); state.graph.fromJSON(JSON.parse(data)); }; func.handle_save_files = function(files) { state.filepath = files[0].path; func.do_save(); }; func.ts_data = function(nodes, name) { let declarations = []; let addDeclaration = (dec) => { if (declarations.indexOf(dec) == -1) { declarations.push(dec); } } addDeclaration("let tree : DialogueTree = new DialogueTree(" + JSON.stringify(name) + ");\n"); let finalString = ""; for (let i = 0; i < nodes.length; i++) { finalString += "\n\n"; let node = nodes[i]; if (node.type == "Node") { finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n"; finalString += "node.setName(" + JSON.stringify(node.name) + ");\n"; if (node.next != null && node.next != undefined) { finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addNode(node);\n"; addDeclaration("let node : DialogueNode;\n"); } else if (node.type == "GoToLabel") { finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n"; finalString += "node.setNext(" + JSON.stringify(node.name) + ");\n"; finalString += "tree.addNode(node);\n"; addDeclaration("let node : DialogueNode;\n"); } else if (node.type == "Tree") { finalString += "nodetree = new DialogueNodeTree(" + JSON.stringify(node.id) + ");\n"; finalString += "nodetree.setTree(() => { return " + node.name + "; });\n"; if (node.next != null && node.next != undefined) { finalString += "nodetree.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "nodetree.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addNode(nodetree);\n"; addDeclaration("let nodetree : DialogueNodeTree;\n"); } else if (node.type == "StartNode") { finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n"; if (node.next != null && node.next != undefined) { finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addStartNode(node);\n"; addDeclaration("let node : DialogueNode;\n"); } else if (node.type == "Text") { finalString += "text = new DialogueText(" + JSON.stringify(node.id) + ");\n"; //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away! // SetSay needs to be a function, otherwise data might not be updated when printing! finalString += "text.setSay(() => { return new Say(" + node.name + ");});\n"; if (node.next != null && node.next != undefined) { finalString += "text.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "text.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addNode(text);\n"; addDeclaration("let text : DialogueText;\n"); } else if (node.type == "Choice") { finalString += "choice = new DialogueChoice(" + JSON.stringify(node.id) + ");\n"; //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away! // SetSay needs to be a function, otherwise data might not be updated when printing! finalString += "choice.setSay(() => { return new Say(" + node.name + ");});\n"; if (node.value != undefined && node.value != null && node.value != "") { finalString += "choice.setConditions(() => { return " + node.value + ";});\n" } if (node.next != null && node.next != undefined) { finalString += "choice.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "choice.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addNode(choice);\n"; addDeclaration("let choice : DialogueChoice;\n"); } else if (node.type == "Branch") { finalString += "branch = new DialogueBranch(" + JSON.stringify(node.id) + ");\n"; finalString += "branch.setVariable(() => { return " + node.variable + ";});\n"; for (let key in node.branches) { let branchCondition = key; let branchTarget = node.branches[key]; if (branchCondition == "_default") { finalString += "branch.setNext(" + JSON.stringify(branchTarget) + ");\n"; } else { finalString += "branch.addBranch(" + JSON.stringify(branchTarget) + ", () => { return " + branchCondition + ";});\n"; } } finalString += "tree.addNode(branch);\n"; addDeclaration("let branch : DialogueBranch;\n"); } else if (node.type == "Set" || node.type == "Function") { finalString += "set = new DialogueSet(" + JSON.stringify(node.id) + ");\n"; if (node.type == "Function") { finalString += "set.setFunction(() => {" + node.name + "});\n"; } else { finalString += "set.setFunction(() => { " + node.variable + " = " + node.value + ";});\n"; } if (node.next != null && node.next != undefined) { finalString += "set.setNext(" + JSON.stringify(node.next) + ");\n"; } if (node.choices != undefined && node.choices != null) { finalString += "set.setChoices(" + JSON.stringify(node.choices) + ");\n"; } finalString += "tree.addNode(set);\n"; addDeclaration("let set : DialogueSet;\n"); } else if (node.type == "AIHook") { // hooktype, identifier, conditions // AskAbout/Circumstance/Informative/Critical node.hooktype = node.hooktype != undefined ? node.hooktype : ""; node.identifier = node.identifier != undefined ? node.identifier : ""; node.conditions = node.conditions != undefined ? node.conditions : ""; let hookType = node.hooktype.toLowerCase().trim(); finalString += " AI." + (hookType == "askabout" ? "investigateRules" : "talktoRules") + ".createAndAddRule({\n" + " name : " + JSON.stringify("AIHook for " + name) + ",\n"; finalString += " conditions : (runner : RulebookRunner) => {\n" + " // @ts-ignore\n" + " let player = WorldState.player, greeter = runner.noun.greeter, answerer = runner.noun.answerer;\n" + "\n" + " return (" + node.conditions + ");\n" + " },\n"; if (hookType == "critical" || hookType == "informative") { finalString += " priority : Rule.PRIORITY_HIGHEST,\n" + " firstPriority : Rule.PRIORITY_HIGHEST,\n"; } else { finalString += " priority : Rule.PRIORITY_MEDIUM,\n" + " firstPriority : Rule.PRIORITY_MEDIUM,\n" } if (hookType == "informative") { finalString += " code : (runner : RulebookRunner) => {\n" + " runner.noun.runFirst.push(tree);\n" + " },\n"; } else if (hookType == "critical") { finalString += " code : (runner : RulebookRunner) => {\n" + " runner.noun.runAndStop.push(tree);\n" + " },\n"; } else { finalString += " code : (runner : RulebookRunner) => {\n" + " runner.noun.options.push({\n" + " tree: tree,\n" + " text : new Say(" + node.identifier + ")\n" + " })\n" + " },\n"; } finalString += "});\n\n"; } } return declarations.join("") + finalString; }; // Initialize (function() { state.paper = new joint.dia.Paper( { el: $('#paper'), width: 32000, height: 16000, model: state.graph, gridSize: 1, defaultLink: con.default_link, validateConnection: func.validate_connection, validateMagnet: func.validate_magnet, snapLinks: { radius: 75 }, // enable link snapping within 75px lookup radius markAvailable: true, }); state.paper.on('blank:pointerdown', function(e, x, y) { if (e.button === 0 || e.button === 1) { state.panning = true; state.mouse_position.x = e.pageX; state.mouse_position.y = e.pageY; $('body').css('cursor', 'move'); func.apply_fields(); } }); state.paper.on('cell:pointerdown', function(e, x, y) { func.apply_fields(); }); $('#container').mousemove(function(e) { if (state.panning) { var $this = $(this); $this.scrollLeft($this.scrollLeft() + state.mouse_position.x - e.pageX); $this.scrollTop($this.scrollTop() + state.mouse_position.y - e.pageY); state.mouse_position.x = e.pageX; state.mouse_position.y = e.pageY; } }); $('#container').mouseup(function(e) { state.panning = false; $('body').css('cursor', 'default'); }); $('#file_open').on('change', function() { if (this.files) func.handle_open_files(this.files); // clear files from this input var $this = $(this); $this.wrap('
').parent('form').trigger('reset'); $this.unwrap(); }); $('#file_save').on('change', function() { func.handle_save_files(this.files); }); $('body').on('dragenter', function(e) { e.stopPropagation(); e.preventDefault(); }); $('body').on('dragexit', function(e) { e.stopPropagation(); e.preventDefault(); }); $('body').on('dragover', function(e) { e.stopPropagation(); e.preventDefault(); }); $('body').on('drop', function(e) { e.stopPropagation(); e.preventDefault(); func.handle_open_files(e.originalEvent.dataTransfer.files); }); $(window).on('keydown', function(event) { // Catch Ctrl-S or key code 19 on Mac (Cmd-S) if (((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') || event.which === 19) { event.stopPropagation(); event.preventDefault(); func.save(); return false; } else if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 'o') { event.stopPropagation(); event.preventDefault(); func.show_open_dialog(); return false; } return true; }); $(window).resize(function() { func.apply_fields(); var $window = $(window); var $container = $('#container'); $container.height($window.innerHeight()); $container.width($window.innerWidth()); return this; }); $(window).trigger('resize'); // Context menu state.menu = new nw.Menu(); state.menu.append(new nw.MenuItem({ label: 'Text', click: func.add_node(joint.shapes.dialogue.Text) })); state.menu.append(new nw.MenuItem({ label: 'Choice', click: func.add_node(joint.shapes.dialogue.Choice) })); state.menu.append(new nw.MenuItem({ label: 'Branch', click: func.add_node(joint.shapes.dialogue.Branch) })); state.menu.append(new nw.MenuItem({ label: 'Set', click: func.add_node(joint.shapes.dialogue.Set) })); state.menu.append(new nw.MenuItem({ label: 'Function', click: func.add_node(joint.shapes.dialogue.Function) })); state.menu.append(new nw.MenuItem({ label: 'GoToLabel', click: func.add_node(joint.shapes.dialogue.GoToLabel) })); state.menu.append(new nw.MenuItem({ label: 'Tree', click: func.add_node(joint.shapes.dialogue.Tree) })); state.menu.append(new nw.MenuItem({ label: 'Label', click: func.add_node(joint.shapes.dialogue.Node) })); state.menu.append(new nw.MenuItem({ label: 'Start Point', click: func.add_node(joint.shapes.dialogue.StartNode) })); state.menu.append(new nw.MenuItem({ label: 'AI Hook', click: func.add_node(joint.shapes.dialogue.AIHook) })); state.menu.append(new nw.MenuItem({ type: 'separator' })); state.menu.append(new nw.MenuItem({ label: 'Save', click: func.save })); state.menu.append(new nw.MenuItem({ label: 'Open', click: func.show_open_dialog })); state.menu.append(new nw.MenuItem({ label: 'New', click: func.clear })); document.body.addEventListener('contextmenu', function(e) { e.preventDefault(); state.context_position.x = e.x; state.context_position.y = e.y; state.menu.popup(e.x, e.y); return false; }, false); })(); function updateTextareas () { window.requestAnimationFrame(updateTextareas); let textareas = new Array(...document.getElementsByTagName("textarea")); for (let i = 0; i < textareas.length; i++) { let textarea = textareas[i]; textarea.style.height = "0 px"; textarea.style.height = textarea.scrollHeight + "px"; } } window.requestAnimationFrame(updateTextareas);