dialogger.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  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. '<textarea class="name" placeholder="Text"></textarea>',
  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('textarea, 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('textarea.name, 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('textarea.name, 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, 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. size: { width: 350 },
  207. inPorts: ['input'],
  208. outPorts: ['output'],
  209. attrs:
  210. {
  211. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  212. },
  213. },
  214. joint.shapes.dialogue.Base.prototype.defaults
  215. ),
  216. });
  217. joint.shapes.dialogue.TextView = joint.shapes.dialogue.BaseView;
  218. joint.shapes.dialogue.Function = joint.shapes.devs.Model.extend(
  219. {
  220. defaults: joint.util.deepSupplement
  221. (
  222. {
  223. type: 'dialogue.Function',
  224. size: { width: 200, height: 65, },
  225. inPorts: ['input'],
  226. outPorts: ['output'],
  227. attrs:
  228. {
  229. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  230. },
  231. },
  232. joint.shapes.dialogue.Base.prototype.defaults
  233. ),
  234. });
  235. joint.shapes.dialogue.FunctionView = joint.shapes.dialogue.BaseView;
  236. joint.shapes.dialogue.GoToLabel = joint.shapes.devs.Model.extend(
  237. {
  238. defaults: joint.util.deepSupplement
  239. (
  240. {
  241. type: 'dialogue.GoToLabel',
  242. size: { width: 125, height: 65, },
  243. inPorts: ['input'],
  244. outPorts: ['output'],
  245. attrs:
  246. {
  247. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  248. },
  249. },
  250. joint.shapes.dialogue.Base.prototype.defaults
  251. ),
  252. });
  253. joint.shapes.dialogue.GoToLabelView = joint.shapes.dialogue.BaseView;
  254. joint.shapes.dialogue.Branch = joint.shapes.devs.Model.extend(
  255. {
  256. defaults: joint.util.deepSupplement
  257. (
  258. {
  259. type: 'dialogue.Branch',
  260. size: { width: 150, height: 95, },
  261. inPorts: ['input'],
  262. outPorts: ['output0'],
  263. values: [],
  264. },
  265. joint.shapes.dialogue.Base.prototype.defaults
  266. ),
  267. });
  268. joint.shapes.dialogue.BranchView = joint.shapes.dialogue.BaseView.extend(
  269. {
  270. template:
  271. [
  272. '<div class="node">',
  273. '<span class="label"></span>',
  274. '<button class="delete">x</button>',
  275. '<button class="add">+</button>',
  276. '<button class="remove">-</button>',
  277. '<input type="text" class="name" placeholder="Variable" />',
  278. '<input type="text" value="Default" readonly/>',
  279. '</div>',
  280. ].join(''),
  281. initialize: function()
  282. {
  283. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  284. this.$box.find('.add').on('click', _.bind(this.addPort, this));
  285. this.$box.find('.remove').on('click', _.bind(this.removePort, this));
  286. },
  287. removePort: function()
  288. {
  289. if (this.model.get('outPorts').length > 1)
  290. {
  291. var outPorts = this.model.get('outPorts').slice(0);
  292. outPorts.pop();
  293. this.model.set('outPorts', outPorts);
  294. var values = this.model.get('values').slice(0);
  295. values.pop();
  296. this.model.set('values', values);
  297. this.updateSize();
  298. }
  299. },
  300. addPort: function()
  301. {
  302. var outPorts = this.model.get('outPorts').slice(0);
  303. outPorts.push('output' + outPorts.length.toString());
  304. this.model.set('outPorts', outPorts);
  305. var values = this.model.get('values').slice(0);
  306. values.push(null);
  307. this.model.set('values', values);
  308. this.updateSize();
  309. },
  310. updateBox: function()
  311. {
  312. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  313. var values = this.model.get('values');
  314. var valueFields = this.$box.find('input.value');
  315. // Add value fields if necessary
  316. for (var i = valueFields.length; i < values.length; i++)
  317. {
  318. // Prevent paper from handling pointerdown.
  319. var $field = $('<input type="text" class="value" />');
  320. $field.attr('placeholder', 'Value ' + (i + 1).toString());
  321. $field.attr('index', i);
  322. this.$box.append($field);
  323. $field.on('mousedown click', function(evt) { evt.stopPropagation(); });
  324. // This is an example of reacting on the input change and storing the input data in the cell model.
  325. $field.on('change', _.bind(function(evt)
  326. {
  327. var values = this.model.get('values').slice(0);
  328. values[$(evt.target).attr('index')] = $(evt.target).val();
  329. this.model.set('values', values);
  330. }, this));
  331. }
  332. // Remove value fields if necessary
  333. for (var i = values.length; i < valueFields.length; i++)
  334. $(valueFields[i]).remove();
  335. // Update value fields
  336. valueFields = this.$box.find('input.value');
  337. for (var i = 0; i < valueFields.length; i++)
  338. {
  339. var field = $(valueFields[i]);
  340. if (!field.is(':focus'))
  341. field.val(values[i]);
  342. }
  343. },
  344. updateSize: function()
  345. {
  346. var textField = this.$box.find('input.name');
  347. var height = textField.outerHeight(true);
  348. this.model.set('size', { width: 150, height: 95 + Math.max(0, (this.model.get('outPorts').length - 1) * height) });
  349. },
  350. });
  351. joint.shapes.dialogue.StartNode = joint.shapes.devs.Model.extend(
  352. {
  353. defaults: joint.util.deepSupplement
  354. (
  355. {
  356. type: 'dialogue.StartNode',
  357. inPorts: [],
  358. outPorts: ['output'],
  359. size: { width: 100, height: 30, },
  360. value: 'DialogueNode.START',
  361. attrs:
  362. {
  363. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  364. },
  365. },
  366. joint.shapes.dialogue.Base.prototype.defaults
  367. ),
  368. });
  369. joint.shapes.dialogue.StartNodeView = joint.shapes.dialogue.BaseView.extend(
  370. {
  371. template:
  372. [
  373. '<div class="node">',
  374. '<span class="label"></span>',
  375. '<button class="delete">x</button>',
  376. '</div>',
  377. ].join(''),
  378. initialize: function()
  379. {
  380. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  381. //this.$box.find('input.value').on('change', _.bind(function(evt)
  382. //{
  383. // this.model.set('value', $(evt.target).val());
  384. //}, this));
  385. },
  386. updateBox: function()
  387. {
  388. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  389. //var field = this.$box.find('input.value');
  390. //if (!field.is(':focus'))
  391. // field.val(this.model.get('value'));
  392. },
  393. });
  394. joint.shapes.dialogue.Choice = joint.shapes.devs.Model.extend(
  395. {
  396. defaults: joint.util.deepSupplement
  397. (
  398. {
  399. type: 'dialogue.Choice',
  400. inPorts: ['input'],
  401. outPorts: ['output'],
  402. size: { width: 175, height: 95, },
  403. value: '',
  404. },
  405. joint.shapes.dialogue.Base.prototype.defaults
  406. ),
  407. });
  408. joint.shapes.dialogue.ChoiceView = joint.shapes.dialogue.BaseView.extend(
  409. {
  410. template:
  411. [
  412. '<div class="node">',
  413. '<span class="label"></span>',
  414. '<button class="delete">x</button>',
  415. '<input type="text" class="name" placeholder="Text" />',
  416. '<input type="text" class="value" placeholder="Conditions" />',
  417. '</div>',
  418. ].join(''),
  419. initialize: function()
  420. {
  421. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  422. this.$box.find('input.value').on('change', _.bind(function(evt)
  423. {
  424. this.model.set('value', $(evt.target).val());
  425. }, this));
  426. },
  427. updateBox: function()
  428. {
  429. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  430. var field = this.$box.find('input.value');
  431. if (!field.is(':focus'))
  432. field.val(this.model.get('value'));
  433. },
  434. });
  435. joint.shapes.dialogue.Set = joint.shapes.devs.Model.extend(
  436. {
  437. defaults: joint.util.deepSupplement
  438. (
  439. {
  440. type: 'dialogue.Set',
  441. inPorts: ['input'],
  442. outPorts: ['output'],
  443. size: { width: 125, height: 90, },
  444. value: '',
  445. },
  446. joint.shapes.dialogue.Base.prototype.defaults
  447. ),
  448. });
  449. joint.shapes.dialogue.SetView = joint.shapes.dialogue.BaseView.extend(
  450. {
  451. template:
  452. [
  453. '<div class="node">',
  454. '<span class="label"></span>',
  455. '<button class="delete">x</button>',
  456. '<input type="text" class="name" placeholder="Variable" />',
  457. '<input type="text" class="value" placeholder="Value" />',
  458. '</div>',
  459. ].join(''),
  460. initialize: function()
  461. {
  462. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  463. this.$box.find('input.value').on('change', _.bind(function(evt)
  464. {
  465. this.model.set('value', $(evt.target).val());
  466. }, this));
  467. },
  468. updateBox: function()
  469. {
  470. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  471. var field = this.$box.find('input.value');
  472. if (!field.is(':focus'))
  473. field.val(this.model.get('value'));
  474. },
  475. });
  476. /**
  477. *
  478. * AI Hook
  479. *
  480. */
  481. joint.shapes.dialogue.AIHook = joint.shapes.devs.Model.extend(
  482. {
  483. defaults: joint.util.deepSupplement
  484. (
  485. {
  486. type: 'dialogue.AIHook',
  487. inPorts: [],
  488. outPorts: [],
  489. size: { width: 300, height: 124, },
  490. value: '',
  491. },
  492. joint.shapes.dialogue.Base.prototype.defaults
  493. ),
  494. });
  495. joint.shapes.dialogue.AIHookView = joint.shapes.dialogue.BaseView.extend(
  496. {
  497. template:
  498. [
  499. '<div class="node">',
  500. '<span class="label"></span>',
  501. '<button class="delete">x</button>',
  502. '<input type="text" class="hooktype" placeholder="AskAbout/Circumstance/Informative/Critical" />',
  503. '<input type="text" class="identifier" placeholder="Selection Text" />',
  504. '<input type="text" class="conditions" placeholder="Conditions (greeter, answerer)" />',
  505. '</div>',
  506. ].join(''),
  507. initialize: function()
  508. {
  509. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  510. this.$box.find('input.conditions').on('change', _.bind(function(evt)
  511. {
  512. this.model.set('conditions', $(evt.target).val());
  513. }, this));
  514. this.$box.find('input.hooktype').on('change', _.bind(function(evt)
  515. {
  516. this.model.set('hooktype', $(evt.target).val());
  517. }, this));
  518. this.$box.find('input.identifier').on('change', _.bind(function(evt)
  519. {
  520. this.model.set('identifier', $(evt.target).val());
  521. }, this));
  522. },
  523. updateBox: function()
  524. {
  525. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  526. var field = this.$box.find('input.hooktype');
  527. if (!field.is(':focus'))
  528. field.val(this.model.get('hooktype'));
  529. var field = this.$box.find('input.conditions');
  530. if (!field.is(':focus'))
  531. field.val(this.model.get('conditions'));
  532. var field = this.$box.find('input.identifier');
  533. if (!field.is(':focus'))
  534. field.val(this.model.get('identifier'));
  535. },
  536. });
  537. // Functions
  538. var func = {};
  539. func.validate_connection = function(cellViewS, magnetS, cellViewT, magnetT, end, linkView)
  540. {
  541. // Prevent loop linking
  542. if (magnetS === magnetT)
  543. return false;
  544. if (cellViewS === cellViewT)
  545. return false;
  546. if ($(magnetT).parents('.outPorts').length > 0) // Can't connect to an output port
  547. return false;
  548. var sourceType = cellViewS.model.attributes.type;
  549. var targetType = cellViewT.model.attributes.type;
  550. var valid = false;
  551. for (var i = 0; i < con.allowable_connections.length; i++)
  552. {
  553. var rule = con.allowable_connections[i];
  554. if (sourceType === rule[0] && targetType === rule[1])
  555. {
  556. valid = true;
  557. break;
  558. }
  559. }
  560. if (!valid)
  561. return false;
  562. var links = state.graph.getConnectedLinks(cellViewS.model);
  563. for (var i = 0; i < links.length; i++)
  564. {
  565. var link = links[i];
  566. if (link.attributes.source.id === cellViewS.model.id && link.attributes.source.port === magnetS.attributes.port.nodeValue && link.attributes.target.id)
  567. {
  568. var targetCell = state.graph.getCell(link.attributes.target.id);
  569. if (targetCell.attributes.type !== targetType)
  570. return false; // We can only connect to multiple targets of the same type
  571. if (targetCell === cellViewT.model)
  572. return false; // Already connected
  573. }
  574. }
  575. return true;
  576. };
  577. func.validate_magnet = function(cellView, magnet)
  578. {
  579. if ($(magnet).parents('.outPorts').length === 0)
  580. return false; // we only want output ports
  581. // If unlimited connections attribute is null, we can only ever connect to one object
  582. // If it is not null, it is an array of type strings which are allowed to have unlimited connections
  583. var unlimitedConnections = magnet.getAttribute('unlimitedConnections');
  584. var links = state.graph.getConnectedLinks(cellView.model);
  585. for (var i = 0; i < links.length; i++)
  586. {
  587. var link = links[i];
  588. if (link.attributes.source.id === cellView.model.id && link.attributes.source.port === magnet.attributes.port.nodeValue)
  589. {
  590. // This port already has a connection
  591. if (unlimitedConnections && link.attributes.target.id)
  592. {
  593. var targetCell = state.graph.getCell(link.attributes.target.id);
  594. if (unlimitedConnections.indexOf(targetCell.attributes.type) !== -1)
  595. return true; // It's okay because this target type has unlimited connections
  596. }
  597. return false;
  598. }
  599. }
  600. return true;
  601. };
  602. func.optimized_data = function()
  603. {
  604. var cells = state.graph.toJSON().cells;
  605. var nodesByID = {};
  606. var cellsByID = {};
  607. var nodes = [];
  608. for (var i = 0; i < cells.length; i++)
  609. {
  610. var cell = cells[i];
  611. if (cell.type != 'link')
  612. {
  613. var node =
  614. {
  615. type: cell.type.slice('dialogue.'.length),
  616. id: cell.id,
  617. };
  618. if (node.type === 'Branch')
  619. {
  620. node.variable = cell.name;
  621. node.branches = {};
  622. for (var j = 0; j < cell.values.length; j++)
  623. {
  624. var branch = cell.values[j];
  625. node.branches[branch] = null;
  626. }
  627. }
  628. else if (node.type === 'Set')
  629. {
  630. node.variable = cell.name;
  631. node.value = cell.value;
  632. node.next = null;
  633. }
  634. else if (node.type === 'AIHook')
  635. {
  636. node.hooktype = cell.hooktype;
  637. node.identifier = cell.identifier;
  638. node.conditions = cell.conditions;
  639. node.next = null;
  640. }
  641. else
  642. {
  643. node.name = cell.name;
  644. node.next = null;
  645. }
  646. if (node.type === "Choice") {
  647. node.value = cell.value;
  648. }
  649. nodes.push(node);
  650. nodesByID[cell.id] = node;
  651. cellsByID[cell.id] = cell;
  652. }
  653. }
  654. for (var i = 0; i < cells.length; i++)
  655. {
  656. var cell = cells[i];
  657. if (cell.type === 'link')
  658. {
  659. var source = nodesByID[cell.source.id];
  660. var target = cell.target ? nodesByID[cell.target.id] : null;
  661. if (source)
  662. {
  663. if (source.type === 'Branch')
  664. {
  665. var portNumber = parseInt(cell.source.port.slice('output'.length));
  666. var value;
  667. if (portNumber === 0)
  668. value = '_default';
  669. else
  670. {
  671. var sourceCell = cellsByID[source.id];
  672. value = sourceCell.values[portNumber - 1];
  673. }
  674. source.branches[value] = target ? target.id : null;
  675. }
  676. else if ((source.type === 'Text' || source.type === 'Node' || source.type === 'StartNode') && target && target.type === 'Choice')
  677. {
  678. if (!source.choices)
  679. {
  680. source.choices = [];
  681. delete source.next;
  682. }
  683. source.choices.push(target.id);
  684. }
  685. else
  686. source.next = target ? target.id : null;
  687. }
  688. }
  689. }
  690. return nodes;
  691. };
  692. // Menu actions
  693. func.flash = function(text)
  694. {
  695. var $flash = $('#flash');
  696. $flash.text(text);
  697. $flash.stop(true, true);
  698. $flash.show();
  699. $flash.css('opacity', 1.0);
  700. $flash.fadeOut({ duration: 1500 });
  701. };
  702. func.apply_fields = function()
  703. {
  704. $('input[type=text], select').blur();
  705. };
  706. func.save = function()
  707. {
  708. func.apply_fields();
  709. if (!state.filepath)
  710. func.show_save_dialog();
  711. else
  712. func.do_save();
  713. };
  714. func.optimized_filename = function(f)
  715. {
  716. return f.substring(0, f.length - 2) + 'dlz';
  717. };
  718. func.perfect_typescript = function(f)
  719. {
  720. return f.substring(0, f.length - 2) + 'dl.ts';
  721. };
  722. func.just_name = function (f) {
  723. return f.substring(0, f.length - 3);
  724. };
  725. func.do_save = function()
  726. {
  727. if (state.filepath)
  728. {
  729. fs.writeFileSync(state.filepath, JSON.stringify(state.graph), 'utf8');
  730. //fs.writeFileSync(func.optimized_filename(state.filepath), JSON.stringify(func.optimized_data()), 'utf8');
  731. let name = func.just_name(func.filename_from_filepath(state.filepath));
  732. let cleants = "// File created automatically by custom Dialogger on " + new Date().toLocaleString() + "\n";
  733. 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.";
  734. cleants += "\nmodule DialogueTrees {\n export let " + name + " = (function () {\n";
  735. cleants += func.ts_data(func.optimized_data(), name).replace(/^/gm, ' ') + "\n return tree;\n })();\n}";
  736. fs.writeFileSync(func.perfect_typescript(state.filepath), cleants, 'utf8');
  737. func.flash('Saved ' + state.filepath);
  738. }
  739. };
  740. func.filename_from_filepath = function(f)
  741. {
  742. return f.replace(/^.*[\\\/]/, '');
  743. };
  744. func.show_open_dialog = function()
  745. {
  746. $('#file_open').click();
  747. };
  748. func.show_save_dialog = function()
  749. {
  750. $('#file_save').click();
  751. };
  752. func.add_node = function(constructor)
  753. {
  754. return function()
  755. {
  756. var container = $('#container')[0];
  757. var element = new constructor(
  758. {
  759. position: { x: state.context_position.x + container.scrollLeft, y: state.context_position.y + container.scrollTop },
  760. });
  761. state.graph.addCells([element]);
  762. };
  763. };
  764. func.clear = function()
  765. {
  766. state.graph.clear();
  767. state.filepath = null;
  768. document.title = 'Dialogger';
  769. };
  770. func.handle_open_files = function(files)
  771. {
  772. state.filepath = files[0].path;
  773. var data = fs.readFileSync(state.filepath);
  774. document.title = func.filename_from_filepath(state.filepath);
  775. state.graph.clear();
  776. state.graph.fromJSON(JSON.parse(data));
  777. };
  778. func.handle_save_files = function(files)
  779. {
  780. state.filepath = files[0].path;
  781. func.do_save();
  782. };
  783. func.ts_data = function(nodes, name) {
  784. let declarations = [];
  785. let addDeclaration = (dec) => {
  786. if (declarations.indexOf(dec) == -1) {
  787. declarations.push(dec);
  788. }
  789. }
  790. addDeclaration("let tree : DialogueTree = new DialogueTree(" + JSON.stringify(name) + ");\n");
  791. let finalString = "";
  792. for (let i = 0; i < nodes.length; i++) {
  793. finalString += "\n\n";
  794. let node = nodes[i];
  795. if (node.type == "Node") {
  796. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  797. finalString += "node.setName(" + JSON.stringify(node.name) + ");\n";
  798. if (node.next != null && node.next != undefined) {
  799. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  800. }
  801. if (node.choices != undefined && node.choices != null) {
  802. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  803. }
  804. finalString += "tree.addNode(node);\n";
  805. addDeclaration("let node : DialogueNode;\n");
  806. } else if (node.type == "GoToLabel") {
  807. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  808. finalString += "node.setNext(" + JSON.stringify(node.name) + ");\n";
  809. finalString += "tree.addNode(node);\n";
  810. addDeclaration("let node : DialogueNode;\n");
  811. } else if (node.type == "Tree") {
  812. finalString += "nodetree = new DialogueNodeTree(" + JSON.stringify(node.id) + ");\n";
  813. finalString += "nodetree.setTree(() => { return " + node.name + "; });\n";
  814. if (node.next != null && node.next != undefined) {
  815. finalString += "nodetree.setNext(" + JSON.stringify(node.next) + ");\n";
  816. }
  817. if (node.choices != undefined && node.choices != null) {
  818. finalString += "nodetree.setChoices(" + JSON.stringify(node.choices) + ");\n";
  819. }
  820. finalString += "tree.addNode(nodetree);\n";
  821. addDeclaration("let nodetree : DialogueNodeTree;\n");
  822. } else if (node.type == "StartNode") {
  823. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  824. if (node.next != null && node.next != undefined) {
  825. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  826. }
  827. if (node.choices != undefined && node.choices != null) {
  828. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  829. }
  830. finalString += "tree.addStartNode(node);\n";
  831. addDeclaration("let node : DialogueNode;\n");
  832. } else if (node.type == "Text") {
  833. finalString += "text = new DialogueText(" + JSON.stringify(node.id) + ");\n";
  834. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  835. // SetSay needs to be a function, otherwise data might not be updated when printing!
  836. finalString += "text.setSay(() => { return new Say(" + node.name + ");});\n";
  837. if (node.next != null && node.next != undefined) {
  838. finalString += "text.setNext(" + JSON.stringify(node.next) + ");\n";
  839. }
  840. if (node.choices != undefined && node.choices != null) {
  841. finalString += "text.setChoices(" + JSON.stringify(node.choices) + ");\n";
  842. }
  843. finalString += "tree.addNode(text);\n";
  844. addDeclaration("let text : DialogueText;\n");
  845. } else if (node.type == "Choice") {
  846. finalString += "choice = new DialogueChoice(" + JSON.stringify(node.id) + ");\n";
  847. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  848. // SetSay needs to be a function, otherwise data might not be updated when printing!
  849. finalString += "choice.setSay(() => { return new Say(" + node.name + ");});\n";
  850. if (node.value != undefined && node.value != null && node.value != "") {
  851. finalString += "choice.setConditions(() => { return " + node.value + ";});\n"
  852. }
  853. if (node.next != null && node.next != undefined) {
  854. finalString += "choice.setNext(" + JSON.stringify(node.next) + ");\n";
  855. }
  856. if (node.choices != undefined && node.choices != null) {
  857. finalString += "choice.setChoices(" + JSON.stringify(node.choices) + ");\n";
  858. }
  859. finalString += "tree.addNode(choice);\n";
  860. addDeclaration("let choice : DialogueChoice;\n");
  861. } else if (node.type == "Branch") {
  862. finalString += "branch = new DialogueBranch(" + JSON.stringify(node.id) + ");\n";
  863. finalString += "branch.setVariable(() => { return " + node.variable + ";});\n";
  864. for (let key in node.branches) {
  865. let branchCondition = key;
  866. let branchTarget = node.branches[key];
  867. if (branchCondition == "_default") {
  868. finalString += "branch.setNext(" + JSON.stringify(branchTarget) + ");\n";
  869. } else {
  870. finalString += "branch.addBranch(" + JSON.stringify(branchTarget) + ", () => { return " + branchCondition + ";});\n";
  871. }
  872. }
  873. finalString += "tree.addNode(branch);\n";
  874. addDeclaration("let branch : DialogueBranch;\n");
  875. } else if (node.type == "Set" || node.type == "Function") {
  876. finalString += "set = new DialogueSet(" + JSON.stringify(node.id) + ");\n";
  877. if (node.type == "Function") {
  878. finalString += "set.setFunction(() => {" + node.name + "});\n";
  879. } else {
  880. finalString += "set.setFunction(() => { " + node.variable + " = " + node.value + ";});\n";
  881. }
  882. if (node.next != null && node.next != undefined) {
  883. finalString += "set.setNext(" + JSON.stringify(node.next) + ");\n";
  884. }
  885. if (node.choices != undefined && node.choices != null) {
  886. finalString += "set.setChoices(" + JSON.stringify(node.choices) + ");\n";
  887. }
  888. finalString += "tree.addNode(set);\n";
  889. addDeclaration("let set : DialogueSet;\n");
  890. } else if (node.type == "AIHook") {
  891. // hooktype, identifier, conditions
  892. // AskAbout/Circumstance/Informative/Critical
  893. node.hooktype = node.hooktype != undefined ? node.hooktype : "";
  894. node.identifier = node.identifier != undefined ? node.identifier : "";
  895. node.conditions = node.conditions != undefined ? node.conditions : "";
  896. let hookType = node.hooktype.toLowerCase().trim();
  897. finalString += " AI." + (hookType == "askabout" ? "investigateRules" : "talktoRules") + ".createAndAddRule({\n" +
  898. " name : " + JSON.stringify("AIHook for " + name) + ",\n";
  899. finalString += " conditions : (runner : RulebookRunner<TalkingHeads>) => {\n" +
  900. " // @ts-ignore\n" +
  901. " let player = WorldState.player, greeter = runner.noun.greeter, answerer = runner.noun.answerer;\n" +
  902. "\n" +
  903. " return (" + node.conditions + ");\n" +
  904. " },\n";
  905. if (hookType == "critical" || hookType == "informative") {
  906. finalString += " priority : Rule.PRIORITY_HIGHEST,\n" +
  907. " firstPriority : Rule.PRIORITY_HIGHEST,\n";
  908. } else {
  909. finalString += " priority : Rule.PRIORITY_MEDIUM,\n" +
  910. " firstPriority : Rule.PRIORITY_MEDIUM,\n"
  911. }
  912. if (hookType == "informative") {
  913. finalString += " code : (runner : RulebookRunner<TalkingHeads>) => {\n" +
  914. " runner.noun.runFirst.push(tree);\n" +
  915. " },\n";
  916. } else if (hookType == "critical") {
  917. finalString += " code : (runner : RulebookRunner<TalkingHeads>) => {\n" +
  918. " runner.noun.runAndStop.push(tree);\n" +
  919. " },\n";
  920. } else {
  921. finalString += " code : (runner : RulebookRunner<TalkingHeads>) => {\n" +
  922. " runner.noun.options.push({\n" +
  923. " tree: tree,\n" +
  924. " text : new Say(" + node.identifier + ")\n" +
  925. " })\n" +
  926. " },\n";
  927. }
  928. finalString += "});\n\n";
  929. }
  930. }
  931. return declarations.join("") + finalString;
  932. };
  933. // Initialize
  934. (function()
  935. {
  936. state.paper = new joint.dia.Paper(
  937. {
  938. el: $('#paper'),
  939. width: 32000,
  940. height: 16000,
  941. model: state.graph,
  942. gridSize: 1,
  943. defaultLink: con.default_link,
  944. validateConnection: func.validate_connection,
  945. validateMagnet: func.validate_magnet,
  946. snapLinks: { radius: 75 }, // enable link snapping within 75px lookup radius
  947. markAvailable: true,
  948. });
  949. state.paper.on('blank:pointerdown', function(e, x, y)
  950. {
  951. if (e.button === 0 || e.button === 1)
  952. {
  953. state.panning = true;
  954. state.mouse_position.x = e.pageX;
  955. state.mouse_position.y = e.pageY;
  956. $('body').css('cursor', 'move');
  957. func.apply_fields();
  958. }
  959. });
  960. state.paper.on('cell:pointerdown', function(e, x, y)
  961. {
  962. func.apply_fields();
  963. });
  964. $('#container').mousemove(function(e)
  965. {
  966. if (state.panning)
  967. {
  968. var $this = $(this);
  969. $this.scrollLeft($this.scrollLeft() + state.mouse_position.x - e.pageX);
  970. $this.scrollTop($this.scrollTop() + state.mouse_position.y - e.pageY);
  971. state.mouse_position.x = e.pageX;
  972. state.mouse_position.y = e.pageY;
  973. }
  974. });
  975. $('#container').mouseup(function(e)
  976. {
  977. state.panning = false;
  978. $('body').css('cursor', 'default');
  979. });
  980. $('#file_open').on('change', function()
  981. {
  982. if (this.files)
  983. func.handle_open_files(this.files);
  984. // clear files from this input
  985. var $this = $(this);
  986. $this.wrap('<form>').parent('form').trigger('reset');
  987. $this.unwrap();
  988. });
  989. $('#file_save').on('change', function()
  990. {
  991. func.handle_save_files(this.files);
  992. });
  993. $('body').on('dragenter', function(e)
  994. {
  995. e.stopPropagation();
  996. e.preventDefault();
  997. });
  998. $('body').on('dragexit', function(e)
  999. {
  1000. e.stopPropagation();
  1001. e.preventDefault();
  1002. });
  1003. $('body').on('dragover', function(e)
  1004. {
  1005. e.stopPropagation();
  1006. e.preventDefault();
  1007. });
  1008. $('body').on('drop', function(e)
  1009. {
  1010. e.stopPropagation();
  1011. e.preventDefault();
  1012. func.handle_open_files(e.originalEvent.dataTransfer.files);
  1013. });
  1014. $(window).on('keydown', function(event)
  1015. {
  1016. // Catch Ctrl-S or key code 19 on Mac (Cmd-S)
  1017. if (((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') || event.which === 19)
  1018. {
  1019. event.stopPropagation();
  1020. event.preventDefault();
  1021. func.save();
  1022. return false;
  1023. }
  1024. else if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 'o')
  1025. {
  1026. event.stopPropagation();
  1027. event.preventDefault();
  1028. func.show_open_dialog();
  1029. return false;
  1030. }
  1031. return true;
  1032. });
  1033. $(window).resize(function()
  1034. {
  1035. func.apply_fields();
  1036. var $window = $(window);
  1037. var $container = $('#container');
  1038. $container.height($window.innerHeight());
  1039. $container.width($window.innerWidth());
  1040. return this;
  1041. });
  1042. $(window).trigger('resize');
  1043. // Context menu
  1044. state.menu = new nw.Menu();
  1045. state.menu.append(new nw.MenuItem({ label: 'Text', click: func.add_node(joint.shapes.dialogue.Text) }));
  1046. state.menu.append(new nw.MenuItem({ label: 'Choice', click: func.add_node(joint.shapes.dialogue.Choice) }));
  1047. state.menu.append(new nw.MenuItem({ label: 'Branch', click: func.add_node(joint.shapes.dialogue.Branch) }));
  1048. state.menu.append(new nw.MenuItem({ label: 'Set', click: func.add_node(joint.shapes.dialogue.Set) }));
  1049. state.menu.append(new nw.MenuItem({ label: 'Function', click: func.add_node(joint.shapes.dialogue.Function) }));
  1050. state.menu.append(new nw.MenuItem({ label: 'GoToLabel', click: func.add_node(joint.shapes.dialogue.GoToLabel) }));
  1051. state.menu.append(new nw.MenuItem({ label: 'Tree', click: func.add_node(joint.shapes.dialogue.Tree) }));
  1052. state.menu.append(new nw.MenuItem({ label: 'Label', click: func.add_node(joint.shapes.dialogue.Node) }));
  1053. state.menu.append(new nw.MenuItem({ label: 'Start Point', click: func.add_node(joint.shapes.dialogue.StartNode) }));
  1054. state.menu.append(new nw.MenuItem({ label: 'AI Hook', click: func.add_node(joint.shapes.dialogue.AIHook) }));
  1055. state.menu.append(new nw.MenuItem({ type: 'separator' }));
  1056. state.menu.append(new nw.MenuItem({ label: 'Save', click: func.save }));
  1057. state.menu.append(new nw.MenuItem({ label: 'Open', click: func.show_open_dialog }));
  1058. state.menu.append(new nw.MenuItem({ label: 'New', click: func.clear }));
  1059. document.body.addEventListener('contextmenu', function(e)
  1060. {
  1061. e.preventDefault();
  1062. state.context_position.x = e.x;
  1063. state.context_position.y = e.y;
  1064. state.menu.popup(e.x, e.y);
  1065. return false;
  1066. }, false);
  1067. })();
  1068. function updateTextareas () {
  1069. window.requestAnimationFrame(updateTextareas);
  1070. let textareas = new Array(...document.getElementsByTagName("textarea"));
  1071. for (let i = 0; i < textareas.length; i++) {
  1072. let textarea = textareas[i];
  1073. textarea.style.height = "0 px";
  1074. textarea.style.height = textarea.scrollHeight + "px";
  1075. }
  1076. }
  1077. window.requestAnimationFrame(updateTextareas);