diff --git a/examples/divert-to-choice/main.hank b/examples/divert-to-choice/main.hank new file mode 100644 index 0000000..156b227 --- /dev/null +++ b/examples/divert-to-choice/main.hank @@ -0,0 +1,30 @@ +~ var target=null; +-> start +== start +Mind: {if (unreachable_knot.choice2) "blown" else "not blown"} +{sequence: + ~target = ->unreachable_knot.choice1;| + ~target = ->unreachable_knot.choice2;| + ~target = ->unreachable_knot.choice3; +} + +-> @target + + +== unreachable_knot + +This text should be like a ninja. Completely silent. + +* (choice1) Choice 1 [Ninja!] has been made. +The content following choice 1 will be played out. +** Including deeper choices! +** Beep Boop +* (choice2) Choice 2 has been made. +* (choice3) [Ninja!] + -> finish + +- ->start + +== finish + +Is your mind blown yet? \ No newline at end of file diff --git a/examples/divert-to-choice/test1.hlog b/examples/divert-to-choice/test1.hlog new file mode 100644 index 0000000..97f4306 --- /dev/null +++ b/examples/divert-to-choice/test1.hlog @@ -0,0 +1,10 @@ +Mind: not blown +Choice 1 has been made. +The content following choice 1 will be played out. +* Including deeper choices! +* Beep Boop +> 1: Including deeper choices! +Mind: not blown +Choice 2 has been made. +Mind: blown +Is your mind blown yet? \ No newline at end of file diff --git a/hank/Alt.hx b/hank/Alt.hx index bf273f0..07dc992 100644 --- a/hank/Alt.hx +++ b/hank/Alt.hx @@ -36,8 +36,9 @@ class Alt { public static function parse(buffer: HankBuffer): Option { var rawExpr = buffer.findNestedExpression('{', '}').unwrap().checkValue(); - // trace(rawExpr); - var expr = rawExpr.substr(1, rawExpr.length-2); + + var expr = rawExpr.substr(1, rawExpr.length-2).ltrim(); + // trace (expr); // Sequences are the default behavior var behavior = Sequence; @@ -45,14 +46,22 @@ class Alt { if (expr.startsWith(prefix)) { expr = expr.substr(prefix.length).trim(); behavior = behaviorMap[prefix]; + break; // <-- Finally figured that one out. } } + // trace (behavior); var outputsBuffer = HankBuffer.Dummy(expr); var eachOutputExpr = outputsBuffer.rootSplit('|'); + + + if (eachOutputExpr.length == 1) { return None; // If no pipe is present, it's not an alt } - var outputs = [for(outputExpr in eachOutputExpr) Output.parse(HankBuffer.Dummy(outputExpr), true)]; + // There can't be newlines preceding the arms of alt expressions, or everything following the newline will disappear -- hence ltrim() below. + var outputs = [for(outputExpr in eachOutputExpr) Output.parse(HankBuffer.Dummy(outputExpr.ltrim()), true)]; + + // trace(outputs); buffer.take(rawExpr.length); return Some(new Alt(behavior, outputs)); diff --git a/hank/HInterface.hx b/hank/HInterface.hx index bac6a83..340849b 100644 --- a/hank/HInterface.hx +++ b/hank/HInterface.hx @@ -42,7 +42,7 @@ class HankInterp extends Interp { } // TODO Divert target variables are just StoryNode values case EUnop("->", true, e): - trace(e); + // trace(e); var targetWithDots = ''; var trailingDot = false; while (true) { @@ -50,6 +50,7 @@ class HankInterp extends Interp { case EIdent(lastTarget): targetWithDots = lastTarget + '.' + targetWithDots; if (trailingDot) targetWithDots = targetWithDots.substr(0,targetWithDots.length-1); + // trace('final target is $targetWithDots'); var node = story.resolveNodeInScope(targetWithDots); trace (node); return node; diff --git a/hank/Output.hx b/hank/Output.hx index 1e58149..be004a7 100644 --- a/hank/Output.hx +++ b/hank/Output.hx @@ -11,7 +11,8 @@ import hank.Alt.AltInstance; enum OutputType { Text(t: String); // Pure text that is always displayed AltExpression(a: Alt); - HExpression(h: String); // An embedded Haxe expression whose value will be inserted + HExpression(h: String); // A Haxe expression inside braces whose value will be inserted + HCall(h: String); // An embedded Haxe expression preceded by ~. Will not be inserted InlineDivert(t: String); // A divert statement on the same line as an output sequence. ToggleOutput(o: Output, invert: Bool); // Output that will sometimes be displayed (i.e. [bracketed] section in a choice text or the section following the bracketed section) } @@ -78,11 +79,20 @@ class Output { if (peekLine.length < endSegment) { var text = buffer.takeLine().unwrap(); if (text.length > 0) { +if (text.ltrim().startsWith('~')) + parts.push(HCall(text.ltrim().substr(1))); +else parts.push(Text(text)); } break; } else { var text = buffer.take(endSegment); + // If text starts with ~, it's actually a silent HCall. + if (text.ltrim().startsWith('~')) + parts.push(HCall(text.ltrim().substr(1))); + + else + parts.push(Text(text)); } } else { @@ -168,6 +178,8 @@ class Output { diverted = false; // Clear the diverted flag + // trace(parts); + for (part in parts) { switch (part) { case Text(t): @@ -196,7 +208,9 @@ class Output { else { fullOutput += value; } - + case HCall(h): + hInterface.runEmbeddedHaxe(h,scope); + // Don't append anything case InlineDivert(t): // follow the divert. If the next expression is an output, concatenate the pieces together. Otherwise, terminate formatting story.divertTo(t); diff --git a/hank/Story.hx b/hank/Story.hx index 4acd4ff..6272842 100644 --- a/hank/Story.hx +++ b/hank/Story.hx @@ -297,6 +297,8 @@ class Story { var newScopes = if (target.startsWith("@")) hInterface.getVariable(target.substr(1)) else resolveNodeInScope(target); + // trace('$target is $newScopes'); + var targetIdx = newScopes[0].astIndex; if (targetIdx == -1) { @@ -361,8 +363,16 @@ class Story { // if the choice has a label, increment its view count switch (choice.label) { case Some(l): - var node = resolveNodeInScope(l)[0]; - viewCounts[node] += 1; + var node = switch (resolveNodeInScope(l)) { + case []: + // the choice is being diverted to, which means it's out of scope. Find it from the StoryTree's choice map another way + storyTree.nodeForChoice(choice.id); + + case nodePath: + nodePath[0]; + }; + + viewCounts[node] += 1; case None: } weaveDepth = choice.depth + 1; diff --git a/hank/StoryTestCase.hx b/hank/StoryTestCase.hx index 5441709..40a08a7 100644 --- a/hank/StoryTestCase.hx +++ b/hank/StoryTestCase.hx @@ -138,6 +138,9 @@ class StoryTestCase extends utest.Test { // Make the choice given, and check for expected output. line = line.substr(1).ltrim(); var firstColonIdx = line.indexOf(':'); + if (firstColonIdx == -1) { + throw 'Choice line in transcript does not specify expected output: > $line'; + } var index = Std.parseInt(line.substr(0, firstColonIdx))-1; var expectedOutput = line.substr(firstColonIdx+1).trim(); // trace('expecting: ${expectedOutput}'); diff --git a/hank/StoryTree.hx b/hank/StoryTree.hx index 4e4915e..2c169c8 100644 --- a/hank/StoryTree.hx +++ b/hank/StoryTree.hx @@ -7,7 +7,14 @@ import hank.LogUtil.watch; class StoryNode { public var astIndex(default, null): Int; var children: Map = new Map(); - + /** + The root StoryNode maps all choice IDs to the choices' nodes, so that when diverting to choices, we can easily resolve out-of-scope targets to increment their view counts + */ + var choiceNodes: Map = new Map(); + public function nodeForChoice(id: Int) { + return choiceNodes[id]; + } + public function toString() { return '{Node@${astIndex}: ${[for(key in children.keys()) key]}}'; } @@ -68,7 +75,27 @@ class StoryNode { var node = new StoryNode(exprIndex); lastKnot.addChild(name, node); lastStitch = node; - case EGather(Some(name), _, _) | EChoice({id: _, onceOnly: _, label: Some(name), condition: _, depth: _, output: _, divertTarget: _}): + case EGather(Some(name), _, _): + + leafNode(exprIndex, root, lastKnot, lastStitch, name); + + case EChoice({id: choiceId, onceOnly: _, label: Some(name), condition: _, depth: _, output: _, divertTarget: _}): + + var choiceNode = leafNode(exprIndex, root, lastKnot, lastStitch, name); + + // keep a map of choiceId to node + root.choiceNodes[choiceId] = choiceNode; + + default: + + } + exprIndex++; + } + + return root; + } + + static function leafNode(exprIndex, root, lastKnot, lastStitch, name) { var node = new StoryNode(exprIndex); if (lastKnot == null) { // watch(root); @@ -80,12 +107,6 @@ class StoryNode { // watch(lastStitch); lastStitch.addChild(name, node); } - default: - - } - exprIndex++; - } - - return root; - } + return node; + } } \ No newline at end of file