Options:';
- if (node.options && node.options.length > 0) {
- node.options.forEach(function(opt, idx) {
- html += '
';
- html += '' + esc(opt.text || '') + ' \u2192 ' + esc(opt.goto || '') + '';
- if (opt.condition) html += ' [cond]';
- if (opt.action) html += ' [action]';
- html += ' ';
- html += '
';
+// talktree.js — Full-featured conversation tree editor.
+// Inline editing for all TalkNode and TalkOption fields.
+// No prompt() dialogs. Clause-based condition editor, action forms.
+
+(function() {
+ 'use strict';
+
+ // ---- Path helpers ----
+
+ function talkRoot() { return window._talkData && window._talkData.talk; }
+ function talkNodes() { var r = talkRoot(); return r && r.nodes; }
+ function talkSync() { if (typeof window.onTalkTreeChange === 'function') window.onTalkTreeChange(window._talkData); }
+
+ function talkGet(path) {
+ var parts = path.split('.'), obj = talkRoot();
+ for (var i = 0; i < parts.length; i++) {
+ if (obj == null) return undefined;
+ var k = parts[i];
+ obj = /^\d+$/.test(k) ? obj[parseInt(k, 10)] : obj[k];
+ }
+ return obj;
+ }
+
+ function talkSet(path, value) {
+ var parts = path.split('.'), root = talkRoot();
+ for (var i = 0; i < parts.length - 1; i++) {
+ var k = parts[i];
+ if (/^\d+$/.test(k)) {
+ var idx = parseInt(k, 10);
+ if (root[idx] == null) root[idx] = /^\d+$/.test(parts[i + 1]) ? [] : {};
+ root = root[idx];
+ } else {
+ if (root[k] == null) root[k] = /^\d+$/.test(parts[i + 1]) ? [] : {};
+ root = root[k];
+ }
+ }
+ var lk = parts[parts.length - 1];
+ if (/^\d+$/.test(lk)) { root[parseInt(lk, 10)] = value; }
+ else { root[lk] = value; }
+ }
+
+ function talkDel(path) {
+ var parts = path.split('.'), root = talkRoot();
+ for (var i = 0; i < parts.length - 1; i++) {
+ if (root == null) return;
+ var k = parts[i];
+ root = /^\d+$/.test(k) ? root[parseInt(k, 10)] : root[k];
+ }
+ if (!root) return;
+ var lk = parts[parts.length - 1];
+ if (/^\d+$/.test(lk)) { root.splice(parseInt(lk, 10), 1); }
+ else { delete root[lk]; }
+ }
+
+ // ---- Clause types ----
+
+ var CLAUSE_TYPES = [
+ { key: 'flag', label: 'Flag', hasValue: true },
+ { key: 'player_flag', label: 'Player Flag', hasValue: true },
+ { key: 'has_item', label: 'Has Item', hasValue: false },
+ { key: 'min_credits', label: 'Min Credits', hasValue: false }
+ ];
+
+ function clauseTypeLabel(key) {
+ for (var i = 0; i < CLAUSE_TYPES.length; i++) {
+ if (CLAUSE_TYPES[i].key === key) return CLAUSE_TYPES[i].label;
+ }
+ return key;
+ }
+
+ function clauseTypeHasValue(key) {
+ for (var i = 0; i < CLAUSE_TYPES.length; i++) {
+ if (CLAUSE_TYPES[i].key === key) return CLAUSE_TYPES[i].hasValue;
+ }
+ return false;
+ }
+
+ // Extract clauses from a condition object
+ function extractClauses(cond) {
+ if (!cond || typeof cond !== 'object' || Array.isArray(cond)) return { mode: 'none', clauses: [], groupKey: null };
+ if (cond.all_of && Array.isArray(cond.all_of) && cond.all_of.length > 0) {
+ return { mode: 'multi', clauses: cond.all_of.slice(), groupKey: 'all_of' };
+ }
+ if (cond.any_of && Array.isArray(cond.any_of) && cond.any_of.length > 0) {
+ return { mode: 'multi', clauses: cond.any_of.slice(), groupKey: 'any_of' };
+ }
+ // Single clause from flat fields
+ for (var i = 0; i < CLAUSE_TYPES.length; i++) {
+ if (cond[CLAUSE_TYPES[i].key] !== undefined && cond[CLAUSE_TYPES[i].key] !== '') {
+ var c = {};
+ c[CLAUSE_TYPES[i].key] = cond[CLAUSE_TYPES[i].key];
+ if (cond.not) c.not = cond.not;
+ if (cond.value !== undefined && cond.value !== '') c.value = cond.value;
+ if (cond.has_item && CLAUSE_TYPES[i].key !== 'has_item') c.has_item = cond.has_item;
+ if (cond.player_flag && CLAUSE_TYPES[i].key !== 'player_flag') c.player_flag = cond.player_flag;
+ if (cond.flag && CLAUSE_TYPES[i].key !== 'flag') c.flag = cond.flag;
+ return { mode: 'single', clauses: [c], groupKey: null };
+ }
+ }
+ // Check for has_item or min_credits as the only key
+ for (var j = 0; j < CLAUSE_TYPES.length; j++) {
+ var ck = CLAUSE_TYPES[j].key;
+ if (cond[ck] !== undefined && cond[ck] !== '') {
+ var cc = {};
+ cc[ck] = cond[ck];
+ if (cond.not) cc.not = cond.not;
+ if (cond.value !== undefined && cond.value !== '') cc.value = cond.value;
+ return { mode: 'single', clauses: [cc], groupKey: null };
+ }
+ }
+ return { mode: 'none', clauses: [], groupKey: null };
+ }
+
+ // Determine the type key for a clause object
+ function clauseObjType(clause) {
+ for (var i = 0; i < CLAUSE_TYPES.length; i++) {
+ var k = CLAUSE_TYPES[i].key;
+ if (clause[k] !== undefined && clause[k] !== '') return k;
+ }
+ return null;
+ }
+
+ // Build a full condition object from clauses
+ function buildCondition(clauses, groupKey) {
+ if (!clauses || clauses.length === 0) return null;
+ if (clauses.length === 1) {
+ var flat = {};
+ var c = clauses[0];
+ for (var k in c) { if (c[k] !== undefined && c[k] !== '') flat[k] = c[k]; }
+ return flat;
+ }
+ var cond = {};
+ cond[groupKey || 'all_of'] = clauses.map(function(c) {
+ var sub = {};
+ for (var k in c) { if (c[k] !== undefined && c[k] !== '') sub[k] = c[k]; }
+ return sub;
});
- } else {
- html += '
No options
';
+ return cond;
+ }
+
+ // ---- Action field definitions ----
+
+ var ACTION_FIELDS = [
+ { key: 'give_item', label: 'Give Item', type: 'text' },
+ { key: 'take_item', label: 'Take Item', type: 'text' },
+ { key: 'teleport', label: 'Teleport Room', type: 'number' },
+ { key: 'heal', label: 'Heal HP', type: 'number' },
+ { key: 'cost', label: 'Cost (Credits)', type: 'number' },
+ { key: 'reputation_cost', label: 'Rep Cost', type: 'number' },
+ ];
+
+ var ACTION_BOOLS = [
+ { key: 'assign_task', label: 'Assign Task' },
+ { key: 'skip_task', label: 'Skip Task' },
+ { key: 'extend_task', label: 'Extend Task' },
+ { key: 'sawmill', label: 'Sawmill' },
+ { key: 'aps_node', label: 'APS Node' },
+ ];
+
+ // ---- Event delegation ----
+
+ function talkEvent(e) {
+ var el = e.target;
+ var path = el.getAttribute('data-tp');
+ if (!path) return;
+
+ var tag = el.tagName;
+ var type = el.type;
+
+ if (tag === 'INPUT' || tag === 'TEXTAREA') {
+ var val;
+ if (type === 'checkbox') { val = el.checked; }
+ else if (type === 'number') { var n = parseFloat(el.value); val = isNaN(n) ? 0 : n; }
+ else { val = el.value; }
+ talkSyncPath(path, val);
+ } else if (tag === 'SELECT') {
+ talkSyncPath(path, el.value);
+ }
+ }
+
+ function talkSyncPath(path, val) {
+ talkSet(path, val);
+ talkSync();
}
- html += '';
- html += '
';
- html += '
';
+ function talkHandleClick(e) {
+ var el = e.target;
+ var action = el.getAttribute('data-action');
+ if (!action) return;
- if (node.options) {
- node.options.forEach(function(opt) {
- if (opt.goto && nodes[opt.goto]) {
- html += renderTreeNodes(nodes, opt.goto, seen);
+ var nodeKey = el.getAttribute('data-nk');
+ var optIdx = el.getAttribute('data-oi');
+ var subIdx = el.getAttribute('data-si');
+ var subKey = el.getAttribute('data-sk');
+ var condGroupKey = el.getAttribute('data-cgk');
+
+ switch (action) {
+ case 'delete-node':
+ deleteNode(nodeKey);
+ break;
+ case 'add-option':
+ addOption(nodeKey);
+ break;
+ case 'delete-option':
+ deleteOption(nodeKey, parseInt(optIdx, 10));
+ break;
+ case 'add-node':
+ addNode();
+ break;
+ case 'add-sequence':
+ addSequenceLine(nodeKey);
+ break;
+ case 'delete-sequence':
+ deleteSequenceLine(nodeKey, parseInt(optIdx, 10));
+ break;
+ case 'add-condition':
+ toggleCondition(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null);
+ break;
+ case 'remove-condition':
+ removeCondition(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null);
+ break;
+ case 'add-action':
+ addAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null);
+ break;
+ case 'remove-action':
+ removeAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null);
+ break;
+ case 'add-clause':
+ addClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-ct'));
+ break;
+ case 'remove-clause':
+ removeClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, parseInt(subIdx, 10));
+ break;
+ case 'set-cond-group':
+ setCondGroup(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, condGroupKey);
+ break;
+ case 'add-kv':
+ addKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk'));
+ break;
+ case 'remove-kv':
+ removeKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk'), subKey);
+ break;
+ case 'add-shop-item':
+ addShopItem(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null);
+ break;
+ case 'remove-shop-item':
+ removeShopItem(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, parseInt(subIdx, 10));
+ break;
+ }
+ }
+
+ // ---- Render entry point ----
+
+ window.renderTalkTree = function(data) {
+ var container = document.getElementById('talktree');
+ if (!container) return;
+ window._talkData = data;
+ if (!data || !data.talk || !data.talk.nodes) {
+ container.innerHTML = '
No conversation tree defined.
';
+ return;
+ }
+ var nodes = data.talk.nodes;
+ var visited = {};
+ var html = '
';
+ html += '';
+ html += renderTreeNodes(nodes, 'start', visited);
+
+ var allKeys = Object.keys(nodes);
+ var orphanKeys = [];
+ for (var i = 0; i < allKeys.length; i++) {
+ if (!visited[allKeys[i]]) orphanKeys.push(allKeys[i]);
+ }
+ if (orphanKeys.length > 0) {
+ html += '
';
+ html += '
Unreferenced Nodes
';
+ html += '
These nodes are not reachable from start. Add a goto to them from another node\'s option.
';
+ for (var j = 0; j < orphanKeys.length; j++) {
+ html += renderOrphanNode(orphanKeys[j], nodes[orphanKeys[j]], nodes);
}
+ html += '
';
+ }
+
+ html += renderAddNodeRow();
+ html += '