5 Commits aab543c0b9 ... 36537f10be

Auteur SHA1 Bericht Datum
  Reddo 36537f10be Halfway through actual dialogue 5 jaren geleden
  Reddo d856402647 Allow AIHooks for conversations 5 jaren geleden
  Reddo e2ae7ecbbd Clearer Priority 5 jaren geleden
  Reddo 4b564416eb Nothing much 5 jaren geleden
  Reddo a45aadd082 There we have it. Proper reactions. 5 jaren geleden

+ 21 - 10
app/Controls/Controls.ts

@@ -1,27 +1,38 @@
 module Controls {
-    export function createBigButton (text : string, resolve : (t : string) => void) {
+    export function createBigButton (text : string, index : number, resolve : (t : number) => void) {
         let p = document.createElement("p");
         p.classList.add("choice");
         p.appendChild(document.createTextNode(text));
 
+        markButton(p, index, resolve);
+
+        return p;
+    }
+
+    function markButton (p : HTMLElement, index : number, resolve : (t : number) => void) {
         p.addEventListener("click", () => {
-            resolve(text);
+            resolve(index);
         });
 
         Controls.KeyHandler.applyCode(p, Controls.KeyHandler.getFirstKeyCode());
-
-        return p;
     }
 
-    export async function giveChoices (big? : boolean, ...choices : Array<string>) {
+    export async function giveChoices (big? : boolean, ...choices : Array<Say | string>) : Promise<Array<any>> {
         let buttons;
-        let chosenPromise = <Promise<string>> new Promise((async (resolve) => {
+        let chosenPromise = <Promise<number>> new Promise((async (resolve) => {
             Controls.KeyHandler.reset();
             let say = new Say();
 
-            choices.forEach(choice => {
-                say.add(createBigButton(choice, resolve))
-            });
+            for (let i = 0; i < choices.length; i++) {
+                let choice = choices[i];
+                if (choice instanceof Say) {
+                    let button = (await choice.getHTML("p", ["choice"], true))[0];
+                    markButton(button, i, resolve);
+                    say.add(button);
+                } else {
+                    say.add(createBigButton(choice, i, resolve))
+                }
+            }
 
             buttons = await say.getHTMLContent();
             Elements.CurrentTurnHandler.print(...(buttons));
@@ -29,6 +40,6 @@ module Controls {
 
         let chosen = await chosenPromise;
         Elements.CurrentTurnHandler.unprint(...buttons);
-        return [chosen, choices.indexOf(chosen)];
+        return [choices[chosen], chosen];
     }
 }

+ 4 - 0
app/Elements/Classes/Say.ts

@@ -44,6 +44,10 @@ class Say {
         }
     }
 
+    public static YouThem (target : Thing, uppercase = true) {
+        return Say.Mention(target, uppercase);
+    }
+
     public static YourTheir (target : Thing, uppercase = true) {
         if (target == WorldState.player) {
             return [new SayYour(uppercase)];

+ 38 - 5
app/World/Classes/AI.ts

@@ -14,8 +14,21 @@ interface AIOptions {
     retaliates? : boolean;
 }
 
+interface DialogueHook {
+    text : Say;
+    tree : DialogueTree;
+}
+
+interface TalkingHeads {
+    greeter : Person;
+    answerer : Person;
+    options : Array<DialogueHook>;
+    runFirst : Array<DialogueTree>;
+    runAndStop : Array<DialogueTree>;
+}
+
 class AI {
-    public actor : Thing;
+    public actor : Person;
     public wanderer = true;
     public wandersOn : Region;
     public wanderChance = 50;
@@ -38,8 +51,13 @@ class AI {
     public extraRules : Array<Rulebook<Thing>> = [];
     public static combatRules = new Rulebook<Thing>("Default AI Combat Rules");
     public extraCombatRules : Array<Rulebook<Thing>> = [];
-    public static talktoRules = new Rulebook<Thing>("Default Talk To Rules");
-    public extraTalktoRules : Array<Rulebook<Thing>> = [];
+    public static talktoRules = new Rulebook<TalkingHeads>("Default Talk To Rules");
+    public extraTalktoRules : Array<Rulebook<TalkingHeads>> = [];
+    public static investigateRules = new Rulebook<TalkingHeads>("Default Ask About Rules");
+    public extraInvestigateRules : Array<Rulebook<TalkingHeads>> = [];
+    public static reacttoRules = new Rulebook<Thing>("Default React To Rules");
+    public extraReacttoRules : Array<Rulebook<Thing>> = [];
+    public reactingTo : Action;
 
     public storedReaction : Action;
 
@@ -168,12 +186,27 @@ class AI {
         }
     }
 
-    // TODO: Make this a rulebook. This happens every time an aggressive action is done.
     public getPoked (action : Action) {
+        this.reactingTo = action;
         if (this.actor instanceof Person) {
-            AIRules.getPoked(this.actor, action);
+            //AIRules.getPoked(this.actor, action);
+            return AI.reacttoRules.execute({
+                noun : this.actor
+            }, ...this.extraReacttoRules);
         }
     }
+
+    public answerTo (noun : TalkingHeads) {
+        return AI.talktoRules.execute({
+            noun : noun
+        }, ...this.extraTalktoRules);
+    }
+
+    public interrogateTo (noun : TalkingHeads) {
+        return AI.investigateRules.execute({
+            noun : noun
+        }, ...this.extraInvestigateRules);
+    }
 }
 
 module AIRules {

+ 0 - 61
app/World/Classes/AI/AIGrudge.ts

@@ -1,66 +1,5 @@
 /// <reference path="../AI.ts" />
 ///<reference path="../ContentPicker/ContentMarker.ts"/>
 module AIRules {
-    // Only one at a time
-    export var actionMin = new ContentMarker("That pissed me off a little", true);
-    export var actionMed = new ContentMarker("That pissed me off", true);
-    export var actionMax = new ContentMarker("That pissed me off HARD", true);
 
-    // Only one at a time
-    export var resultNotHostile = new ContentMarker("I'm not gonna do anything about it", true);
-    export var resultRetaliate = new ContentMarker("I'll hit you once so you can see how you like it", true);
-    export var resultHostile = new ContentMarker("I'll hit you until you drop dead.", true);
-
-    export function printGrudgeResult (aggressor : Thing, victim : Person, ...markers : Array<ContentMarker>) : Say {
-        let group = new ContentGroup();
-        let unit = new CombatPokeUnit();
-        group.addUnit(unit);
-        unit.setTarget(victim);
-        unit.setAggressor(aggressor);
-        unit.addMarker(...markers);
-
-        if (aggressor == WorldState.player) {
-            victim.AI.warnedTimes++;
-        }
-        return new Say(...CombatPokeDescription.getDescription(group));
-    }
-
-    // TODO: Make a rulebook called "ReactionTo", this is a rule that should be there with a condition of Action = ActingAggressively.
-    export function getPoked (person : Person, action : Action) {
-        if (person.AI.hostileTargets.includes(action.actor)) {
-            return; // Already hostile
-        }
-        person.AI.addHostility(action.actor, action.aggressivenessRating);
-        if (action.actor == WorldState.player) {
-            person.reputation -= action.aggressivenessRating;
-        }
-
-        let ai = person.AI;
-        let response : Say;
-        let gain = ai.grudgeRate * action.aggressivenessRating;
-        let actionLevel = actionMin;
-        let result = resultNotHostile;
-        if (ai.getHostilityTo(action.actor) > 100) {
-            result = resultHostile;
-        } else if (ai.retaliates && ai.getHostilityTo(action.actor) >= (ai.hostileThreshold / 2)) {
-            result = resultRetaliate;
-        }
-        if (gain >= (ai.hostileThreshold / 2)) {
-            actionLevel = actionMax;
-        } else if (gain >= (ai.hostileThreshold / 4)) {
-            actionLevel = actionMed;
-        }
-        response = printGrudgeResult(action.actor, person, actionLevel, result);
-
-        let nAct : Action;
-        if (result == resultRetaliate) {
-            nAct = new ActionAttack(person, action.actor);
-            nAct.finalSay = response.add(Say.PARAGRAPH_BREAK);
-            nAct.finalSayOnEnd = false;
-        } else {
-            nAct = new ActionWait(person);
-            nAct.finalSay = response.add(Say.PARAGRAPH_BREAK);
-        }
-        person.AI.storedReaction = nAct;
-    }
 }

+ 77 - 0
app/World/Classes/AI/Reaction/ReactToAggressive.ts

@@ -0,0 +1,77 @@
+/// <reference path="../../AI.ts" />
+///<reference path="../../ContentPicker/ContentMarker.ts"/>
+module AIRules {
+// Only one at a time
+    export var actionMin = new ContentMarker("That pissed me off a little", true);
+    export var actionMed = new ContentMarker("That pissed me off", true);
+    export var actionMax = new ContentMarker("That pissed me off HARD", true);
+
+    // Only one at a time
+    export var resultNotHostile = new ContentMarker("I'm not gonna do anything about it", true);
+    export var resultRetaliate = new ContentMarker("I'll hit you once so you can see how you like it", true);
+    export var resultHostile = new ContentMarker("I'll hit you until you drop dead.", true);
+
+    export function printGrudgeResult (aggressor : Thing, victim : Person, ...markers : Array<ContentMarker>) : Say {
+        let group = new ContentGroup();
+        let unit = new CombatPokeUnit();
+        group.addUnit(unit);
+        unit.setTarget(victim);
+        unit.setAggressor(aggressor);
+        unit.addMarker(...markers);
+
+        if (aggressor == WorldState.player) {
+            victim.AI.warnedTimes++;
+        }
+        return new Say(...CombatPokeDescription.getDescription(group));
+    }
+
+    export var actedUponAggressively = AI.reacttoRules.createAndAddRule({
+        name : "Reacting to Aggressive action",
+        code : (runner : RulebookRunner<Person>) => {
+            let person = runner.noun;
+            let pai = runner.noun.AI;
+            let action = pai.reactingTo;
+
+            if (person.AI.hostileTargets.includes(action.actor)) {
+                return; // Already hostile
+            }
+            person.AI.addHostility(action.actor, action.aggressivenessRating);
+            if (action.actor == WorldState.player) {
+                person.reputation -= action.aggressivenessRating;
+            }
+
+            let ai = person.AI;
+            let response : Say;
+            let gain = ai.grudgeRate * action.aggressivenessRating;
+            let actionLevel = actionMin;
+            let result = resultNotHostile;
+            if (ai.getHostilityTo(action.actor) > 100) {
+                result = resultHostile;
+            } else if (ai.retaliates && ai.getHostilityTo(action.actor) >= (ai.hostileThreshold / 2)) {
+                result = resultRetaliate;
+            }
+            if (gain >= (ai.hostileThreshold / 2)) {
+                actionLevel = actionMax;
+            } else if (gain >= (ai.hostileThreshold / 4)) {
+                actionLevel = actionMed;
+            }
+            response = printGrudgeResult(action.actor, person, actionLevel, result);
+
+            let nAct : Action;
+            if (result == resultRetaliate) {
+                nAct = new ActionAttack(person, action.actor);
+                nAct.finalSay = response.add(Say.PARAGRAPH_BREAK);
+                nAct.finalSayOnEnd = false;
+            } else {
+                nAct = new ActionWait(person);
+                nAct.finalSay = response.add(Say.PARAGRAPH_BREAK);
+            }
+            person.AI.storedReaction = nAct;
+        },
+        conditions : (runner : RulebookRunner<Person>) => {
+            let pai = runner.noun.AI;
+            let action = pai.reactingTo;
+            return action.actingAgressively;
+        }
+    })
+}

+ 2 - 2
app/World/Classes/Action.ts

@@ -213,11 +213,11 @@ Action.carry.addRule(
         name : "Check any Action - Angery",
         firstPriority : Rule.PRIORITY_LOWEST,
         priority: Rule.PRIORITY_LOWEST,
-        code : (rulebook : RulebookRunner<Action>) => {
+        code : async (rulebook : RulebookRunner<Action>) => {
             let action = <Action> rulebook.noun;
             let target = action.getNoun(0);
             let tai = (<Person> target).AI;
-            tai.getPoked(action);
+            await tai.getPoked(action);
         },
         conditions : (rulebook : RulebookRunner<Action>) => {
             let action = <Action> rulebook.noun;

+ 1 - 0
app/World/Classes/Action/ActionAttack.ts

@@ -247,6 +247,7 @@ Elements.HyperlinkHandler.HyperlinkingRulebook.addRule(new Rule(
     {
         name : "Hyperlink - Attack",
         firstPriority : Rule.PRIORITY_HIGHEST,
+        priority : Rule.PRIORITY_MEDIUM,
         code : (rulebook : RulebookRunner<Thing>) => {
             let thing = <Thing> rulebook.noun;
 

+ 69 - 3
app/World/Classes/Action/ActionTalk.ts

@@ -13,17 +13,82 @@ class ActionTalk extends Action {
     public static carry: Rulebook<ActionTalk> = new Rulebook("Carry out Talking");
 
     public static defaultCarryTalkingRule = ActionTalk.carry.createAndAddRule({
+        name : "Talking - Check with the AI",
+        firstPriority : Rule.PRIORITY_HIGHEST,
+        priority : Rule.PRIORITY_HIGHEST,
+        code : async (rulebook : RulebookRunner<ActionTalk>) => {
+            let action = <ActionGo> rulebook.noun;
+            //let actor = action.actor;
+            let thing = (<Thing>action.getNoun(0));
+
+            if (thing instanceof Person && action.actor instanceof Person) {
+                let runAndStop : Array<DialogueTree> = [];
+                let runAndContinue : Array<DialogueTree> = [];
+                let circumstantialOptions : Array<DialogueHook> = [];
+
+                await thing.AI.answerTo({greeter : action.actor, answerer : thing, options : circumstantialOptions, runAndStop : runAndStop, runFirst : runAndContinue});
+
+                if (runAndStop.length > 0) {
+                    return await runAndStop[0].execute();
+                } else if (runAndContinue.length > 0) {
+                    for (let i = 0; i < runAndContinue.length; i++) {
+                        await runAndContinue[i].execute();
+                    }
+                }
+
+                let investigativeOptions : Array<DialogueHook> = [];
+                await thing.AI.interrogateTo({greeter : action.actor, answerer : thing, options : investigativeOptions, runAndStop : runAndStop, runFirst : runAndContinue});
+
+
+                let choices : Array<Say> = [];
+                let results = [];
+                if (investigativeOptions.length > 0) {
+                    // TODO: Add more textx
+                    choices.push(new Say(new OneOf(OneOf.PURELY_AT_RANDOM, "Ask about...")));
+                    results.push(null);
+                }
+                for (let i = 0; i < circumstantialOptions.length; i++) {
+                    choices.push(circumstantialOptions[i].text);
+                    results.push(circumstantialOptions[i].tree);
+                }
+
+                // TODO: Add more texts
+                choices.push(new Say(new OneOf(OneOf.PURELY_AT_RANDOM, "Goodbye")));
+                results.push(undefined);
+
+
+                let choice = await Controls.giveChoices(true, ...choices);
+
+                if (results[choice[1]] === null) {
+                    choices = [];
+                    results = [];
+                    for (let i = 0; i < investigativeOptions.length; i++) {
+                        choices.push(investigativeOptions[i].text);
+                        results.push(investigativeOptions[i].tree);
+                    }
+                    choice = await Controls.giveChoices(true, ...choices);
+                    if (results[choice[1]] instanceof DialogueTree) {
+                        await (results[choice[1]]).execute();
+                    }
+                } else if (results[choice[1]] instanceof DialogueTree) {
+                    await (results[choice[1]]).execute();
+                }
+            }
+        }
+    });
+
+    public static lastCarryTalkingRule = ActionTalk.carry.createAndAddRule({
         name : "Talking - Doesn't want to talk",
         firstPriority : -1,
         priority : -1,
-        code : (rulebook : RulebookRunner<ActionTake>) => {
+        code : (rulebook : RulebookRunner<ActionTalk>) => {
             let action = <ActionGo> rulebook.noun;
             //let actor = action.actor;
             let thing = (<Thing>action.getNoun(0));
 
             if (thing instanceof Person) {
-                action.say = new Say("It doesn't look like ", new SayHeSheIt(thing), " wants to talk.");
-            } else {
+                action.say = new Say(...Say.Mention(action.actor), " greet", action.actor == WorldState.player ? " " : "s ", ...Say.Mention(thing), ". There is no response.");
+            } else if (action.actor == WorldState.player) {
                 action.say = new Say("How are you going to talk to that?")
             }
         }
@@ -38,6 +103,7 @@ Elements.HyperlinkHandler.HyperlinkingRulebook.addRule(new Rule(
     {
         name : "Hyperlink - Talk",
         firstPriority : Rule.PRIORITY_HIGHEST,
+        priority : Rule.PRIORITY_HIGHEST,
         code : (rulebook : RulebookRunner<Thing>) => {
             let thing = <Thing> rulebook.noun;
 

+ 23 - 23
app/World/Classes/Things/Humanoid/Orc/OrcDebugger.ts

@@ -23,26 +23,26 @@ class OrcDebugger extends Humanoid {
     }
 }
 
-ActionTalk.carry.createAndAddRule({
-    name : "Talking to the orc",
-    firstPriority : ActionTalk.PRIORITY_GLOBAL_DIALOGUE,
-    priority : ActionTalk.PRIORITY_COMMON_DIALOGUE,
-    conditions : (runner : RulebookRunner<ActionTalk>) => {
-        return runner.noun.getNoun(0) instanceof OrcDebugger;
-    },
-    code : (runner : RulebookRunner<ActionTalk>) => {
-        let orc = <OrcDebugger> runner.noun.getNoun(0);
-        //await DialogueTrees.CompilableTest.execute();
-        let result = Dice.testAgainstRoll(
-            {name: "Charm + 2", value : WorldState.player.getStat(Attributes.Charm) + 2},
-            {name: "Orc's wits + 2", value : orc.getStat(Attributes.Intelligence) + 2}
-        );
-
-        if (result > 0) {
-            Elements.CurrentTurnHandler.printAsContent(new Say("You win!"));
-        } else {
-            Elements.CurrentTurnHandler.printAsContent(new Say("You lose."));
-        }
-        return true;
-    }
-});
+// ActionTalk.carry.createAndAddRule({
+//     name : "Talking to the orc",
+//     firstPriority : ActionTalk.PRIORITY_GLOBAL_DIALOGUE,
+//     priority : ActionTalk.PRIORITY_COMMON_DIALOGUE,
+//     conditions : (runner : RulebookRunner<ActionTalk>) => {
+//         return runner.noun.getNoun(0) instanceof OrcDebugger;
+//     },
+//     code : (runner : RulebookRunner<ActionTalk>) => {
+//         let orc = <OrcDebugger> runner.noun.getNoun(0);
+//         //await DialogueTrees.CompilableTest.execute();
+//         let result = Dice.testAgainstRoll(
+//             {name: "Charm + 2", value : WorldState.player.getStat(Attributes.Charm) + 2},
+//             {name: "Orc's wits + 2", value : orc.getStat(Attributes.Intelligence) + 2}
+//         );
+//
+//         if (result > 0) {
+//             Elements.CurrentTurnHandler.printAsContent(new Say("You win!"));
+//         } else {
+//             Elements.CurrentTurnHandler.printAsContent(new Say("You lose."));
+//         }
+//         return true;
+//     }
+// });

+ 4 - 0
app/World/Classes/Things/Person.ts

@@ -198,4 +198,8 @@ class Person extends Thing implements AttributeBearer, SkillBearer {
         this.getRoom().place(corpse);
         OutOfPlay.removeFromPlay(this);
     }
+
+    public getPoked (action : Action) {
+        return this.AI.getPoked(action);
+    }
 }

+ 117 - 2
tools/dialogger/dialogger.js

@@ -388,7 +388,7 @@ joint.shapes.dialogue.StartNode = joint.shapes.devs.Model.extend(
 	(
 		{
 			type: 'dialogue.StartNode',
-			inPorts: ['input'],
+			inPorts: [],
 			outPorts: ['output'],
 			size: { width: 100, height: 30, },
 			value: 'DialogueNode.START',
@@ -518,6 +518,72 @@ joint.shapes.dialogue.SetView = joint.shapes.dialogue.BaseView.extend(
 	},
 });
 
+/**
+ *
+ * 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:
+			[
+				'<div class="node">',
+				'<span class="label"></span>',
+				'<button class="delete">x</button>',
+				'<input type="text" class="hooktype" placeholder="AskAbout/Circumstance/Informative/Critical" />',
+				'<input type="text" class="identifier" placeholder="Selection Text" />',
+				'<input type="text" class="conditions" placeholder="Conditions (greeter, answerer)" />',
+				'</div>',
+			].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 = {};
@@ -626,6 +692,13 @@ func.optimized_data = function()
 				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;
@@ -661,7 +734,7 @@ func.optimized_data = function()
 					}
 					source.branches[value] = target ? target.id : null;
 				}
-				else if ((source.type === 'Text' || source.type === 'Node') && target && target.type === 'Choice')
+				else if ((source.type === 'Text' || source.type === 'Node' || source.type === 'StartNode') && target && target.type === 'Choice')
 				{
 					if (!source.choices)
 					{
@@ -894,6 +967,47 @@ func.ts_data = function(nodes, name) {
 			}
 			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<TalkingHeads>) => {\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<TalkingHeads>) => {\n" +
+					"        runner.noun.runFirst.push(tree);\n" +
+					"    },\n";
+			} else if (hookType == "critical") {
+				finalString += "    code : (runner : RulebookRunner<TalkingHeads>) => {\n" +
+					"        runner.noun.runAndStop.push(tree);\n" +
+					"    },\n";
+			} else {
+				finalString += "    code : (runner : RulebookRunner<TalkingHeads>) => {\n" +
+					"        runner.noun.options.push({\n" +
+					"            tree: tree,\n" +
+					"            text : new Say(" + node.identifier + ")\n" +
+					"        })\n" +
+					"    },\n";
+			}
+			finalString += "});\n\n";
 		}
 	}
 	return declarations.join("") + finalString;
@@ -1035,6 +1149,7 @@ func.ts_data = function(nodes, name) {
 	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 }));