dialogger.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  1. var fs = require('fs');
  2. // Constants
  3. var con =
  4. {
  5. allowable_connections:
  6. [
  7. ['dialogue.Text', 'dialogue.Text'],
  8. ['dialogue.Text', 'dialogue.Node'],
  9. ['dialogue.Text', 'dialogue.Choice'],
  10. ['dialogue.Text', 'dialogue.Set'],
  11. ['dialogue.Text', 'dialogue.Branch'],
  12. ['dialogue.Text', 'dialogue.Function'],
  13. ['dialogue.Text', 'dialogue.Tree'],
  14. ['dialogue.Text', 'dialogue.GoToLabel'],
  15. ['dialogue.Node', 'dialogue.Text'],
  16. ['dialogue.Node', 'dialogue.Node'],
  17. ['dialogue.Node', 'dialogue.Choice'],
  18. ['dialogue.Node', 'dialogue.Set'],
  19. ['dialogue.Node', 'dialogue.Branch'],
  20. ['dialogue.Node', 'dialogue.Function'],
  21. ['dialogue.Node', 'dialogue.Tree'],
  22. ['dialogue.Node', 'dialogue.GoToLabel'],
  23. ['dialogue.Tree', 'dialogue.Node'],
  24. ['dialogue.Tree', 'dialogue.Text'],
  25. ['dialogue.Tree', 'dialogue.Choice'],
  26. ['dialogue.Tree', 'dialogue.Set'],
  27. ['dialogue.Tree', 'dialogue.Branch'],
  28. ['dialogue.Tree', 'dialogue.Function'],
  29. ['dialogue.Tree', 'dialogue.Tree'],
  30. ['dialogue.Tree', 'dialogue.GoToLabel'],
  31. ['dialogue.StartNode', 'dialogue.Text'],
  32. ['dialogue.StartNode', 'dialogue.Node'],
  33. ['dialogue.StartNode', 'dialogue.Choice'],
  34. ['dialogue.StartNode', 'dialogue.Set'],
  35. ['dialogue.StartNode', 'dialogue.Branch'],
  36. ['dialogue.StartNode', 'dialogue.Function'],
  37. ['dialogue.StartNode', 'dialogue.Tree'],
  38. ['dialogue.StartNode', 'dialogue.GoToLabel'],
  39. ['dialogue.Choice', 'dialogue.Text'],
  40. ['dialogue.Choice', 'dialogue.Node'],
  41. ['dialogue.Choice', 'dialogue.Set'],
  42. ['dialogue.Choice', 'dialogue.Branch'],
  43. ['dialogue.Choice', 'dialogue.Function'],
  44. ['dialogue.Choice', 'dialogue.Tree'],
  45. ['dialogue.Choice', 'dialogue.GoToLabel'],
  46. ['dialogue.Set', 'dialogue.Text'],
  47. ['dialogue.Set', 'dialogue.Node'],
  48. ['dialogue.Set', 'dialogue.Set'],
  49. ['dialogue.Set', 'dialogue.Branch'],
  50. ['dialogue.Set', 'dialogue.Function'],
  51. ['dialogue.Set', 'dialogue.Tree'],
  52. ['dialogue.Set', 'dialogue.GoToLabel'],
  53. ['dialogue.Function', 'dialogue.Text'],
  54. ['dialogue.Function', 'dialogue.Node'],
  55. ['dialogue.Function', 'dialogue.Set'],
  56. ['dialogue.Function', 'dialogue.Branch'],
  57. ['dialogue.Function', 'dialogue.Function'],
  58. ['dialogue.Function', 'dialogue.Tree'],
  59. ['dialogue.Function', 'dialogue.GoToLabel'],
  60. ['dialogue.Branch', 'dialogue.Text'],
  61. ['dialogue.Branch', 'dialogue.Node'],
  62. ['dialogue.Branch', 'dialogue.Set'],
  63. ['dialogue.Branch', 'dialogue.Branch'],
  64. ['dialogue.Branch', 'dialogue.Function'],
  65. ['dialogue.Branch', 'dialogue.Tree'],
  66. ['dialogue.Branch', 'dialogue.GoToLabel'],
  67. ],
  68. default_link: new joint.dia.Link(
  69. {
  70. attrs:
  71. {
  72. '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', },
  73. '.link-tools .tool-remove circle, .marker-vertex': { r: 8 },
  74. },
  75. }),
  76. };
  77. con.default_link.set('smooth', true);
  78. // State
  79. var state =
  80. {
  81. graph: new joint.dia.Graph(),
  82. paper: null,
  83. filepath: null,
  84. panning: false,
  85. mouse_position: { x: 0, y: 0 },
  86. context_position: { x: 0, y: 0 },
  87. menu: null,
  88. };
  89. // Models
  90. joint.shapes.dialogue = {};
  91. joint.shapes.dialogue.Base = joint.shapes.devs.Model.extend(
  92. {
  93. defaults: joint.util.deepSupplement
  94. (
  95. {
  96. type: 'dialogue.Base',
  97. size: { width: 100, height: 64 },
  98. name: '',
  99. attrs:
  100. {
  101. rect: { stroke: 'none', 'fill-opacity': 0 },
  102. text: { display: 'none' },
  103. },
  104. },
  105. joint.shapes.devs.Model.prototype.defaults
  106. ),
  107. });
  108. joint.shapes.dialogue.BaseView = joint.shapes.devs.ModelView.extend(
  109. {
  110. template:
  111. [
  112. '<div class="node">',
  113. '<span class="label"></span>',
  114. '<button class="delete">x</button>',
  115. '<input type="text" class="name" placeholder="Text" />',
  116. '</div>',
  117. ].join(''),
  118. initialize: function()
  119. {
  120. _.bindAll(this, 'updateBox');
  121. joint.shapes.devs.ModelView.prototype.initialize.apply(this, arguments);
  122. this.$box = $(_.template(this.template)());
  123. // Prevent paper from handling pointerdown.
  124. this.$box.find('input').on('mousedown click', function(evt) { evt.stopPropagation(); });
  125. // This is an example of reacting on the input change and storing the input data in the cell model.
  126. this.$box.find('input.name').on('change', _.bind(function(evt)
  127. {
  128. this.model.set('name', $(evt.target).val());
  129. }, this));
  130. this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
  131. // Update the box position whenever the underlying model changes.
  132. this.model.on('change', this.updateBox, this);
  133. // Remove the box when the model gets removed from the graph.
  134. this.model.on('remove', this.removeBox, this);
  135. this.updateBox();
  136. },
  137. render: function()
  138. {
  139. joint.shapes.devs.ModelView.prototype.render.apply(this, arguments);
  140. this.paper.$el.prepend(this.$box);
  141. this.updateBox();
  142. return this;
  143. },
  144. updateBox: function()
  145. {
  146. // Set the position and dimension of the box so that it covers the JointJS element.
  147. var bbox = this.model.getBBox();
  148. // Example of updating the HTML with a data stored in the cell model.
  149. var nameField = this.$box.find('input.name');
  150. if (!nameField.is(':focus'))
  151. nameField.val(this.model.get('name'));
  152. var label = this.$box.find('.label');
  153. var type = this.model.get('type').slice('dialogue.'.length);
  154. label.text(type);
  155. label.attr('class', 'label ' + type);
  156. this.$box[0].classList.add(type);
  157. this.$box.css({ width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
  158. },
  159. removeBox: function(evt)
  160. {
  161. this.$box.remove();
  162. },
  163. });
  164. joint.shapes.dialogue.Node = joint.shapes.devs.Model.extend(
  165. {
  166. defaults: joint.util.deepSupplement
  167. (
  168. {
  169. type: 'dialogue.Node',
  170. size: { width: 100, height: 65, },
  171. inPorts: ['input'],
  172. outPorts: ['output'],
  173. attrs:
  174. {
  175. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  176. },
  177. },
  178. joint.shapes.dialogue.Base.prototype.defaults
  179. ),
  180. });
  181. joint.shapes.dialogue.NodeView = joint.shapes.dialogue.BaseView;
  182. joint.shapes.dialogue.Tree = joint.shapes.devs.Model.extend(
  183. {
  184. defaults: joint.util.deepSupplement
  185. (
  186. {
  187. type: 'dialogue.Tree',
  188. size: { width: 150, height: 65, },
  189. inPorts: ['input'],
  190. outPorts: ['output'],
  191. attrs:
  192. {
  193. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  194. },
  195. },
  196. joint.shapes.dialogue.Base.prototype.defaults
  197. ),
  198. });
  199. joint.shapes.dialogue.TreeView = joint.shapes.dialogue.BaseView;
  200. joint.shapes.dialogue.Text = joint.shapes.devs.Model.extend(
  201. {
  202. defaults: joint.util.deepSupplement
  203. (
  204. {
  205. type: 'dialogue.Text',
  206. inPorts: ['input'],
  207. outPorts: ['output'],
  208. attrs:
  209. {
  210. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  211. },
  212. },
  213. joint.shapes.dialogue.Base.prototype.defaults
  214. ),
  215. });
  216. joint.shapes.dialogue.TextView = joint.shapes.dialogue.BaseView;
  217. joint.shapes.dialogue.Function = joint.shapes.devs.Model.extend(
  218. {
  219. defaults: joint.util.deepSupplement
  220. (
  221. {
  222. type: 'dialogue.Function',
  223. size: { width: 200, height: 65, },
  224. inPorts: ['input'],
  225. outPorts: ['output'],
  226. attrs:
  227. {
  228. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  229. },
  230. },
  231. joint.shapes.dialogue.Base.prototype.defaults
  232. ),
  233. });
  234. joint.shapes.dialogue.FunctionView = joint.shapes.dialogue.BaseView;
  235. joint.shapes.dialogue.GoToLabel = joint.shapes.devs.Model.extend(
  236. {
  237. defaults: joint.util.deepSupplement
  238. (
  239. {
  240. type: 'dialogue.GoToLabel',
  241. size: { width: 125, height: 65, },
  242. inPorts: ['input'],
  243. outPorts: ['output'],
  244. attrs:
  245. {
  246. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  247. },
  248. },
  249. joint.shapes.dialogue.Base.prototype.defaults
  250. ),
  251. });
  252. joint.shapes.dialogue.GoToLabelView = joint.shapes.dialogue.BaseView;
  253. joint.shapes.dialogue.Branch = joint.shapes.devs.Model.extend(
  254. {
  255. defaults: joint.util.deepSupplement
  256. (
  257. {
  258. type: 'dialogue.Branch',
  259. size: { width: 150, height: 95, },
  260. inPorts: ['input'],
  261. outPorts: ['output0'],
  262. values: [],
  263. },
  264. joint.shapes.dialogue.Base.prototype.defaults
  265. ),
  266. });
  267. joint.shapes.dialogue.BranchView = joint.shapes.dialogue.BaseView.extend(
  268. {
  269. template:
  270. [
  271. '<div class="node">',
  272. '<span class="label"></span>',
  273. '<button class="delete">x</button>',
  274. '<button class="add">+</button>',
  275. '<button class="remove">-</button>',
  276. '<input type="text" class="name" placeholder="Variable" />',
  277. '<input type="text" value="Default" readonly/>',
  278. '</div>',
  279. ].join(''),
  280. initialize: function()
  281. {
  282. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  283. this.$box.find('.add').on('click', _.bind(this.addPort, this));
  284. this.$box.find('.remove').on('click', _.bind(this.removePort, this));
  285. },
  286. removePort: function()
  287. {
  288. if (this.model.get('outPorts').length > 1)
  289. {
  290. var outPorts = this.model.get('outPorts').slice(0);
  291. outPorts.pop();
  292. this.model.set('outPorts', outPorts);
  293. var values = this.model.get('values').slice(0);
  294. values.pop();
  295. this.model.set('values', values);
  296. this.updateSize();
  297. }
  298. },
  299. addPort: function()
  300. {
  301. var outPorts = this.model.get('outPorts').slice(0);
  302. outPorts.push('output' + outPorts.length.toString());
  303. this.model.set('outPorts', outPorts);
  304. var values = this.model.get('values').slice(0);
  305. values.push(null);
  306. this.model.set('values', values);
  307. this.updateSize();
  308. },
  309. updateBox: function()
  310. {
  311. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  312. var values = this.model.get('values');
  313. var valueFields = this.$box.find('input.value');
  314. // Add value fields if necessary
  315. for (var i = valueFields.length; i < values.length; i++)
  316. {
  317. // Prevent paper from handling pointerdown.
  318. var $field = $('<input type="text" class="value" />');
  319. $field.attr('placeholder', 'Value ' + (i + 1).toString());
  320. $field.attr('index', i);
  321. this.$box.append($field);
  322. $field.on('mousedown click', function(evt) { evt.stopPropagation(); });
  323. // This is an example of reacting on the input change and storing the input data in the cell model.
  324. $field.on('change', _.bind(function(evt)
  325. {
  326. var values = this.model.get('values').slice(0);
  327. values[$(evt.target).attr('index')] = $(evt.target).val();
  328. this.model.set('values', values);
  329. }, this));
  330. }
  331. // Remove value fields if necessary
  332. for (var i = values.length; i < valueFields.length; i++)
  333. $(valueFields[i]).remove();
  334. // Update value fields
  335. valueFields = this.$box.find('input.value');
  336. for (var i = 0; i < valueFields.length; i++)
  337. {
  338. var field = $(valueFields[i]);
  339. if (!field.is(':focus'))
  340. field.val(values[i]);
  341. }
  342. },
  343. updateSize: function()
  344. {
  345. var textField = this.$box.find('input.name');
  346. var height = textField.outerHeight(true);
  347. this.model.set('size', { width: 150, height: 95 + Math.max(0, (this.model.get('outPorts').length - 1) * height) });
  348. },
  349. });
  350. joint.shapes.dialogue.StartNode = joint.shapes.devs.Model.extend(
  351. {
  352. defaults: joint.util.deepSupplement
  353. (
  354. {
  355. type: 'dialogue.StartNode',
  356. inPorts: ['input'],
  357. outPorts: ['output'],
  358. size: { width: 100, height: 30, },
  359. value: 'DialogueNode.START',
  360. attrs:
  361. {
  362. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  363. },
  364. },
  365. joint.shapes.dialogue.Base.prototype.defaults
  366. ),
  367. });
  368. joint.shapes.dialogue.StartNodeView = joint.shapes.dialogue.BaseView.extend(
  369. {
  370. template:
  371. [
  372. '<div class="node">',
  373. '<span class="label"></span>',
  374. '<button class="delete">x</button>',
  375. '</div>',
  376. ].join(''),
  377. initialize: function()
  378. {
  379. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  380. //this.$box.find('input.value').on('change', _.bind(function(evt)
  381. //{
  382. // this.model.set('value', $(evt.target).val());
  383. //}, this));
  384. },
  385. updateBox: function()
  386. {
  387. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  388. //var field = this.$box.find('input.value');
  389. //if (!field.is(':focus'))
  390. // field.val(this.model.get('value'));
  391. },
  392. });
  393. joint.shapes.dialogue.Choice = joint.shapes.devs.Model.extend(
  394. {
  395. defaults: joint.util.deepSupplement
  396. (
  397. {
  398. type: 'dialogue.Choice',
  399. inPorts: ['input'],
  400. outPorts: ['output'],
  401. size: { width: 175, height: 95, },
  402. value: '',
  403. },
  404. joint.shapes.dialogue.Base.prototype.defaults
  405. ),
  406. });
  407. joint.shapes.dialogue.ChoiceView = joint.shapes.dialogue.BaseView.extend(
  408. {
  409. template:
  410. [
  411. '<div class="node">',
  412. '<span class="label"></span>',
  413. '<button class="delete">x</button>',
  414. '<input type="text" class="name" placeholder="Text" />',
  415. '<input type="text" class="value" placeholder="Conditions" />',
  416. '</div>',
  417. ].join(''),
  418. initialize: function()
  419. {
  420. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  421. this.$box.find('input.value').on('change', _.bind(function(evt)
  422. {
  423. this.model.set('value', $(evt.target).val());
  424. }, this));
  425. },
  426. updateBox: function()
  427. {
  428. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  429. var field = this.$box.find('input.value');
  430. if (!field.is(':focus'))
  431. field.val(this.model.get('value'));
  432. },
  433. });
  434. joint.shapes.dialogue.Set = joint.shapes.devs.Model.extend(
  435. {
  436. defaults: joint.util.deepSupplement
  437. (
  438. {
  439. type: 'dialogue.Set',
  440. inPorts: ['input'],
  441. outPorts: ['output'],
  442. size: { width: 125, height: 90, },
  443. value: '',
  444. },
  445. joint.shapes.dialogue.Base.prototype.defaults
  446. ),
  447. });
  448. joint.shapes.dialogue.SetView = joint.shapes.dialogue.BaseView.extend(
  449. {
  450. template:
  451. [
  452. '<div class="node">',
  453. '<span class="label"></span>',
  454. '<button class="delete">x</button>',
  455. '<input type="text" class="name" placeholder="Variable" />',
  456. '<input type="text" class="value" placeholder="Value" />',
  457. '</div>',
  458. ].join(''),
  459. initialize: function()
  460. {
  461. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  462. this.$box.find('input.value').on('change', _.bind(function(evt)
  463. {
  464. this.model.set('value', $(evt.target).val());
  465. }, this));
  466. },
  467. updateBox: function()
  468. {
  469. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  470. var field = this.$box.find('input.value');
  471. if (!field.is(':focus'))
  472. field.val(this.model.get('value'));
  473. },
  474. });
  475. // Functions
  476. var func = {};
  477. func.validate_connection = function(cellViewS, magnetS, cellViewT, magnetT, end, linkView)
  478. {
  479. // Prevent loop linking
  480. if (magnetS === magnetT)
  481. return false;
  482. if (cellViewS === cellViewT)
  483. return false;
  484. if ($(magnetT).parents('.outPorts').length > 0) // Can't connect to an output port
  485. return false;
  486. var sourceType = cellViewS.model.attributes.type;
  487. var targetType = cellViewT.model.attributes.type;
  488. var valid = false;
  489. for (var i = 0; i < con.allowable_connections.length; i++)
  490. {
  491. var rule = con.allowable_connections[i];
  492. if (sourceType === rule[0] && targetType === rule[1])
  493. {
  494. valid = true;
  495. break;
  496. }
  497. }
  498. if (!valid)
  499. return false;
  500. var links = state.graph.getConnectedLinks(cellViewS.model);
  501. for (var i = 0; i < links.length; i++)
  502. {
  503. var link = links[i];
  504. if (link.attributes.source.id === cellViewS.model.id && link.attributes.source.port === magnetS.attributes.port.nodeValue && link.attributes.target.id)
  505. {
  506. var targetCell = state.graph.getCell(link.attributes.target.id);
  507. if (targetCell.attributes.type !== targetType)
  508. return false; // We can only connect to multiple targets of the same type
  509. if (targetCell === cellViewT.model)
  510. return false; // Already connected
  511. }
  512. }
  513. return true;
  514. };
  515. func.validate_magnet = function(cellView, magnet)
  516. {
  517. if ($(magnet).parents('.outPorts').length === 0)
  518. return false; // we only want output ports
  519. // If unlimited connections attribute is null, we can only ever connect to one object
  520. // If it is not null, it is an array of type strings which are allowed to have unlimited connections
  521. var unlimitedConnections = magnet.getAttribute('unlimitedConnections');
  522. var links = state.graph.getConnectedLinks(cellView.model);
  523. for (var i = 0; i < links.length; i++)
  524. {
  525. var link = links[i];
  526. if (link.attributes.source.id === cellView.model.id && link.attributes.source.port === magnet.attributes.port.nodeValue)
  527. {
  528. // This port already has a connection
  529. if (unlimitedConnections && link.attributes.target.id)
  530. {
  531. var targetCell = state.graph.getCell(link.attributes.target.id);
  532. if (unlimitedConnections.indexOf(targetCell.attributes.type) !== -1)
  533. return true; // It's okay because this target type has unlimited connections
  534. }
  535. return false;
  536. }
  537. }
  538. return true;
  539. };
  540. func.optimized_data = function()
  541. {
  542. var cells = state.graph.toJSON().cells;
  543. var nodesByID = {};
  544. var cellsByID = {};
  545. var nodes = [];
  546. for (var i = 0; i < cells.length; i++)
  547. {
  548. var cell = cells[i];
  549. if (cell.type != 'link')
  550. {
  551. var node =
  552. {
  553. type: cell.type.slice('dialogue.'.length),
  554. id: cell.id,
  555. };
  556. if (node.type === 'Branch')
  557. {
  558. node.variable = cell.name;
  559. node.branches = {};
  560. for (var j = 0; j < cell.values.length; j++)
  561. {
  562. var branch = cell.values[j];
  563. node.branches[branch] = null;
  564. }
  565. }
  566. else if (node.type === 'Set')
  567. {
  568. node.variable = cell.name;
  569. node.value = cell.value;
  570. node.next = null;
  571. }
  572. else
  573. {
  574. node.name = cell.name;
  575. node.next = null;
  576. }
  577. if (node.type === "Choice") {
  578. node.value = cell.value;
  579. }
  580. nodes.push(node);
  581. nodesByID[cell.id] = node;
  582. cellsByID[cell.id] = cell;
  583. }
  584. }
  585. for (var i = 0; i < cells.length; i++)
  586. {
  587. var cell = cells[i];
  588. if (cell.type === 'link')
  589. {
  590. var source = nodesByID[cell.source.id];
  591. var target = cell.target ? nodesByID[cell.target.id] : null;
  592. if (source)
  593. {
  594. if (source.type === 'Branch')
  595. {
  596. var portNumber = parseInt(cell.source.port.slice('output'.length));
  597. var value;
  598. if (portNumber === 0)
  599. value = '_default';
  600. else
  601. {
  602. var sourceCell = cellsByID[source.id];
  603. value = sourceCell.values[portNumber - 1];
  604. }
  605. source.branches[value] = target ? target.id : null;
  606. }
  607. else if ((source.type === 'Text' || source.type === 'Node') && target && target.type === 'Choice')
  608. {
  609. if (!source.choices)
  610. {
  611. source.choices = [];
  612. delete source.next;
  613. }
  614. source.choices.push(target.id);
  615. }
  616. else
  617. source.next = target ? target.id : null;
  618. }
  619. }
  620. }
  621. return nodes;
  622. };
  623. // Menu actions
  624. func.flash = function(text)
  625. {
  626. var $flash = $('#flash');
  627. $flash.text(text);
  628. $flash.stop(true, true);
  629. $flash.show();
  630. $flash.css('opacity', 1.0);
  631. $flash.fadeOut({ duration: 1500 });
  632. };
  633. func.apply_fields = function()
  634. {
  635. $('input[type=text], select').blur();
  636. };
  637. func.save = function()
  638. {
  639. func.apply_fields();
  640. if (!state.filepath)
  641. func.show_save_dialog();
  642. else
  643. func.do_save();
  644. };
  645. func.optimized_filename = function(f)
  646. {
  647. return f.substring(0, f.length - 2) + 'dlz';
  648. };
  649. func.perfect_typescript = function(f)
  650. {
  651. return f.substring(0, f.length - 2) + 'dl.ts';
  652. };
  653. func.just_name = function (f) {
  654. return f.substring(0, f.length - 3);
  655. };
  656. func.do_save = function()
  657. {
  658. if (state.filepath)
  659. {
  660. fs.writeFileSync(state.filepath, JSON.stringify(state.graph), 'utf8');
  661. //fs.writeFileSync(func.optimized_filename(state.filepath), JSON.stringify(func.optimized_data()), 'utf8');
  662. let name = func.just_name(func.filename_from_filepath(state.filepath));
  663. let cleants = "// File created automatically by custom Dialogger on " + new Date().toLocaleString() + "\n";
  664. 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.";
  665. cleants += "\nmodule DialogueTrees {\n export let " + name + " = (function () {\n";
  666. cleants += func.ts_data(func.optimized_data(), name).replace(/^/gm, ' ') + "\n return tree;\n })();\n}";
  667. fs.writeFileSync(func.perfect_typescript(state.filepath), cleants, 'utf8');
  668. func.flash('Saved ' + state.filepath);
  669. }
  670. };
  671. func.filename_from_filepath = function(f)
  672. {
  673. return f.replace(/^.*[\\\/]/, '');
  674. };
  675. func.show_open_dialog = function()
  676. {
  677. $('#file_open').click();
  678. };
  679. func.show_save_dialog = function()
  680. {
  681. $('#file_save').click();
  682. };
  683. func.add_node = function(constructor)
  684. {
  685. return function()
  686. {
  687. var container = $('#container')[0];
  688. var element = new constructor(
  689. {
  690. position: { x: state.context_position.x + container.scrollLeft, y: state.context_position.y + container.scrollTop },
  691. });
  692. state.graph.addCells([element]);
  693. };
  694. };
  695. func.clear = function()
  696. {
  697. state.graph.clear();
  698. state.filepath = null;
  699. document.title = 'Dialogger';
  700. };
  701. func.handle_open_files = function(files)
  702. {
  703. state.filepath = files[0].path;
  704. var data = fs.readFileSync(state.filepath);
  705. document.title = func.filename_from_filepath(state.filepath);
  706. state.graph.clear();
  707. state.graph.fromJSON(JSON.parse(data));
  708. };
  709. func.handle_save_files = function(files)
  710. {
  711. state.filepath = files[0].path;
  712. func.do_save();
  713. };
  714. func.ts_data = function(nodes, name) {
  715. let declarations = [];
  716. let addDeclaration = (dec) => {
  717. if (declarations.indexOf(dec) == -1) {
  718. declarations.push(dec);
  719. }
  720. }
  721. addDeclaration("let tree : DialogueTree = new DialogueTree(" + JSON.stringify(name) + ");\n");
  722. let finalString = "";
  723. for (let i = 0; i < nodes.length; i++) {
  724. finalString += "\n\n";
  725. let node = nodes[i];
  726. if (node.type == "Node") {
  727. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  728. finalString += "node.setName(" + JSON.stringify(node.name) + ");\n";
  729. if (node.next != null && node.next != undefined) {
  730. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  731. }
  732. if (node.choices != undefined && node.choices != null) {
  733. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  734. }
  735. finalString += "tree.addNode(node);\n";
  736. addDeclaration("let node : DialogueNode;\n");
  737. } else if (node.type == "GoToLabel") {
  738. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  739. finalString += "node.setNext(" + JSON.stringify(node.name) + ");\n";
  740. finalString += "tree.addNode(node);\n";
  741. addDeclaration("let node : DialogueNode;\n");
  742. } else if (node.type == "Tree") {
  743. finalString += "nodetree = new DialogueNodeTree(" + JSON.stringify(node.id) + ");\n";
  744. finalString += "nodetree.setTree(() => { return " + node.name + "; });\n";
  745. if (node.next != null && node.next != undefined) {
  746. finalString += "nodetree.setNext(" + JSON.stringify(node.next) + ");\n";
  747. }
  748. if (node.choices != undefined && node.choices != null) {
  749. finalString += "nodetree.setChoices(" + JSON.stringify(node.choices) + ");\n";
  750. }
  751. finalString += "tree.addNode(nodetree);\n";
  752. addDeclaration("let nodetree : DialogueNodeTree;\n");
  753. } else if (node.type == "StartNode") {
  754. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  755. if (node.next != null && node.next != undefined) {
  756. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  757. }
  758. if (node.choices != undefined && node.choices != null) {
  759. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  760. }
  761. finalString += "tree.addStartNode(node);\n";
  762. addDeclaration("let node : DialogueNode;\n");
  763. } else if (node.type == "Text") {
  764. finalString += "text = new DialogueText(" + JSON.stringify(node.id) + ");\n";
  765. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  766. // SetSay needs to be a function, otherwise data might not be updated when printing!
  767. finalString += "text.setSay(() => { return new Say(" + node.name + ");});\n";
  768. if (node.next != null && node.next != undefined) {
  769. finalString += "text.setNext(" + JSON.stringify(node.next) + ");\n";
  770. }
  771. if (node.choices != undefined && node.choices != null) {
  772. finalString += "text.setChoices(" + JSON.stringify(node.choices) + ");\n";
  773. }
  774. finalString += "tree.addNode(text);\n";
  775. addDeclaration("let text : DialogueText;\n");
  776. } else if (node.type == "Choice") {
  777. finalString += "choice = new DialogueChoice(" + JSON.stringify(node.id) + ");\n";
  778. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  779. // SetSay needs to be a function, otherwise data might not be updated when printing!
  780. finalString += "choice.setSay(() => { return new Say(" + node.name + ");});\n";
  781. if (node.value != undefined && node.value != null && node.value != "") {
  782. finalString += "choice.setConditions(() => { return " + node.value + ";});\n"
  783. }
  784. if (node.next != null && node.next != undefined) {
  785. finalString += "choice.setNext(" + JSON.stringify(node.next) + ");\n";
  786. }
  787. if (node.choices != undefined && node.choices != null) {
  788. finalString += "choice.setChoices(" + JSON.stringify(node.choices) + ");\n";
  789. }
  790. finalString += "tree.addNode(choice);\n";
  791. addDeclaration("let choice : DialogueChoice;\n");
  792. } else if (node.type == "Branch") {
  793. finalString += "branch = new DialogueBranch(" + JSON.stringify(node.id) + ");\n";
  794. finalString += "branch.setVariable(() => { return " + node.variable + ";});\n";
  795. for (let key in node.branches) {
  796. let branchCondition = key;
  797. let branchTarget = node.branches[key];
  798. if (branchCondition == "_default") {
  799. finalString += "branch.setNext(" + JSON.stringify(branchTarget) + ");\n";
  800. } else {
  801. finalString += "branch.addBranch(" + JSON.stringify(branchTarget) + ", () => { return " + branchCondition + ";});\n";
  802. }
  803. }
  804. finalString += "tree.addNode(branch);\n";
  805. addDeclaration("let branch : DialogueBranch;\n");
  806. } else if (node.type == "Set" || node.type == "Function") {
  807. finalString += "set = new DialogueSet(" + JSON.stringify(node.id) + ");\n";
  808. if (node.type == "Function") {
  809. finalString += "set.setFunction(() => {" + node.name + "});\n";
  810. } else {
  811. finalString += "set.setFunction(() => { " + node.variable + " = " + node.value + ";});\n";
  812. }
  813. if (node.next != null && node.next != undefined) {
  814. finalString += "set.setNext(" + JSON.stringify(node.next) + ");\n";
  815. }
  816. if (node.choices != undefined && node.choices != null) {
  817. finalString += "set.setChoices(" + JSON.stringify(node.choices) + ");\n";
  818. }
  819. finalString += "tree.addNode(set);\n";
  820. addDeclaration("let set : DialogueSet;\n");
  821. }
  822. }
  823. return declarations.join("") + finalString;
  824. };
  825. // Initialize
  826. (function()
  827. {
  828. state.paper = new joint.dia.Paper(
  829. {
  830. el: $('#paper'),
  831. width: 32000,
  832. height: 16000,
  833. model: state.graph,
  834. gridSize: 1,
  835. defaultLink: con.default_link,
  836. validateConnection: func.validate_connection,
  837. validateMagnet: func.validate_magnet,
  838. snapLinks: { radius: 75 }, // enable link snapping within 75px lookup radius
  839. markAvailable: true,
  840. });
  841. state.paper.on('blank:pointerdown', function(e, x, y)
  842. {
  843. if (e.button === 0 || e.button === 1)
  844. {
  845. state.panning = true;
  846. state.mouse_position.x = e.pageX;
  847. state.mouse_position.y = e.pageY;
  848. $('body').css('cursor', 'move');
  849. func.apply_fields();
  850. }
  851. });
  852. state.paper.on('cell:pointerdown', function(e, x, y)
  853. {
  854. func.apply_fields();
  855. });
  856. $('#container').mousemove(function(e)
  857. {
  858. if (state.panning)
  859. {
  860. var $this = $(this);
  861. $this.scrollLeft($this.scrollLeft() + state.mouse_position.x - e.pageX);
  862. $this.scrollTop($this.scrollTop() + state.mouse_position.y - e.pageY);
  863. state.mouse_position.x = e.pageX;
  864. state.mouse_position.y = e.pageY;
  865. }
  866. });
  867. $('#container').mouseup(function(e)
  868. {
  869. state.panning = false;
  870. $('body').css('cursor', 'default');
  871. });
  872. $('#file_open').on('change', function()
  873. {
  874. if (this.files)
  875. func.handle_open_files(this.files);
  876. // clear files from this input
  877. var $this = $(this);
  878. $this.wrap('<form>').parent('form').trigger('reset');
  879. $this.unwrap();
  880. });
  881. $('#file_save').on('change', function()
  882. {
  883. func.handle_save_files(this.files);
  884. });
  885. $('body').on('dragenter', function(e)
  886. {
  887. e.stopPropagation();
  888. e.preventDefault();
  889. });
  890. $('body').on('dragexit', function(e)
  891. {
  892. e.stopPropagation();
  893. e.preventDefault();
  894. });
  895. $('body').on('dragover', function(e)
  896. {
  897. e.stopPropagation();
  898. e.preventDefault();
  899. });
  900. $('body').on('drop', function(e)
  901. {
  902. e.stopPropagation();
  903. e.preventDefault();
  904. func.handle_open_files(e.originalEvent.dataTransfer.files);
  905. });
  906. $(window).on('keydown', function(event)
  907. {
  908. // Catch Ctrl-S or key code 19 on Mac (Cmd-S)
  909. if (((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') || event.which === 19)
  910. {
  911. event.stopPropagation();
  912. event.preventDefault();
  913. func.save();
  914. return false;
  915. }
  916. else if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 'o')
  917. {
  918. event.stopPropagation();
  919. event.preventDefault();
  920. func.show_open_dialog();
  921. return false;
  922. }
  923. return true;
  924. });
  925. $(window).resize(function()
  926. {
  927. func.apply_fields();
  928. var $window = $(window);
  929. var $container = $('#container');
  930. $container.height($window.innerHeight());
  931. $container.width($window.innerWidth());
  932. return this;
  933. });
  934. $(window).trigger('resize');
  935. // Context menu
  936. state.menu = new nw.Menu();
  937. state.menu.append(new nw.MenuItem({ label: 'Text', click: func.add_node(joint.shapes.dialogue.Text) }));
  938. state.menu.append(new nw.MenuItem({ label: 'Choice', click: func.add_node(joint.shapes.dialogue.Choice) }));
  939. state.menu.append(new nw.MenuItem({ label: 'Branch', click: func.add_node(joint.shapes.dialogue.Branch) }));
  940. state.menu.append(new nw.MenuItem({ label: 'Set', click: func.add_node(joint.shapes.dialogue.Set) }));
  941. state.menu.append(new nw.MenuItem({ label: 'Function', click: func.add_node(joint.shapes.dialogue.Function) }));
  942. state.menu.append(new nw.MenuItem({ label: 'GoToLabel', click: func.add_node(joint.shapes.dialogue.GoToLabel) }));
  943. state.menu.append(new nw.MenuItem({ label: 'Tree', click: func.add_node(joint.shapes.dialogue.Tree) }));
  944. state.menu.append(new nw.MenuItem({ label: 'Label', click: func.add_node(joint.shapes.dialogue.Node) }));
  945. state.menu.append(new nw.MenuItem({ label: 'Start Point', click: func.add_node(joint.shapes.dialogue.StartNode) }));
  946. state.menu.append(new nw.MenuItem({ type: 'separator' }));
  947. state.menu.append(new nw.MenuItem({ label: 'Save', click: func.save }));
  948. state.menu.append(new nw.MenuItem({ label: 'Open', click: func.show_open_dialog }));
  949. state.menu.append(new nw.MenuItem({ label: 'New', click: func.clear }));
  950. document.body.addEventListener('contextmenu', function(e)
  951. {
  952. e.preventDefault();
  953. state.context_position.x = e.x;
  954. state.context_position.y = e.y;
  955. state.menu.popup(e.x, e.y);
  956. return false;
  957. }, false);
  958. })();