I debated between manually making sure that longer snippets were broken up into separate dialogs and having the engine handle it. I decided that I would just have to be careful with it for now. I have the final snippet of json and two entities (game is using impact++ and font sugar so it might need some reworking to use with other projects).
Simple conversation:
var conversations = {
// Holds the arc for a specific character
home_zone_01: {
dialogs: [
{
text: "Hello There",
name: "Sam",
npc_actor: 'actor02.png',
npc_actor_frame: [0, 0], // 0 is for initial text, 1 is for response
pc_actor: 'actor01.png',
pc_actor_frame: [0, 1],
responses: [
{
text: "Umm... Me?",
action: function(){
return {
type: "goto",
id: 1
};
}
},
{
text: "Hi!"
}
]
},
{
text: "Yes you!",
name: "Sam",
npc_actor: 'actor02.png',
npc_actor_frame: [0, 0],
pc_actor: 'actor01.png',
pc_actor_frame: [0, 1],
responses: [
// Single Response doesn't have a selection option
{
text: "Nice to meet you!",
action: function(){
conversationFlags.update("home_zone_01", 3);
// No return value so conversation ends
}
}
]
}
],
getConversationIndex: function(){
// Checks the tags and returns an int for the conversation
return 0;
}
}
};
/**
* @conversation.js
* @version: 0.01
* @author: Jeremy Fry
* @date: 2014
* @copyright (c) 2014 Jeremy Fry, under The MIT License (see LICENSE)
*
*/
ig.module(
'plugins.jfry.conversation-engine.conversation'
)
.requires(
'impact.game',
'plugins.jfry.conversation-engine.ui-text-box'
)
.defines(function(){
ig.Conversation = ig.Class.extend({
staticInstantiate: function(i){
return !ig.Conversation.instance ? null : ig.Conversation.instance;
},
init: function(conversationFlags){
ig.Conversation.instance = this;
this.visible = false;
this.activeConversation = null;
this.conversationIndex = null;
this.conversationStore = null;
this.textBoxEntity = null;
this.conversationFlags = conversationFlags || {};
this.active = false;
this.actions = {};
this.pcAnimationSheet = null;
this.npcAnimationSheet = null;
},
startConversation: function(id, conversationStore, dialogIndex){
this.active = true;
if(id === undefined || conversationStore === undefined){
throw new Error("Must pass id and conversationStore");
}
this.conversationIndex = id;
ig.game.pause(true);
if ( ig.game.camera ) {
ig.game.camera.unpause();
}
if(!this.textBoxEntity){
this.textBoxEntity = ig.game.spawnEntity(ig.UITextBox, 0, 0, {});
}
this.conversationStore = conversationStore;
this.activeConversation = this.conversationStore[id];
if(!this.activeConversation){
throw new Error("Conversation "+id+" not found");
}
// Get the current index and send the text off to the textbox entity
this.currentIndex = this.activeConversation.getConversationIndex();
this.textBoxEntity.visible = true;
if(dialogIndex){
this.currentIndex = dialogIndex;
}
this.textBoxEntity.setText(this.activeConversation.dialogs[this.currentIndex].text);
this.textBoxEntity.setName(this.activeConversation.dialogs[this.currentIndex].name || ig.game.playerName);
// Create the binds so we don't have do the checks in the handle input
this.resetActions();
if(this.activeConversation.dialogs[this.currentIndex].action){
this.actions.okay = this.activeConversation.dialogs[this.currentIndex].action.bind(this);
}else if(this.activeConversation.dialogs[this.currentIndex].responses){
this.actions.okay = this.loadResponses.bind(this);
}
this.pcAnimationSheet = new ig.Image('media/'+this.activeConversation.dialogs[this.currentIndex].pc_actor);
this.npcAnimationSheet = new ig.Image('media/'+this.activeConversation.dialogs[this.currentIndex].npc_actor);
this.textBoxEntity.setActors(
this.pcAnimationSheet, this.npcAnimationSheet,
this.activeConversation.dialogs[this.currentIndex].pc_actor_frame[0],
this.activeConversation.dialogs[this.currentIndex].npc_actor_frame[0]
);
},
loadResponses: function(){
var textObj = {};
var keys = ['okay', 'menu', 'action', 'cancel'];
this.resetActions();
this.activeConversation.dialogs[this.currentIndex].responses.forEach(function(value, index, array){
textObj[keys[index]] = value.text;
if(value.action){
this.actions[keys[index]] = value.action.bind(this);
}
}.bind(this));
this.textBoxEntity.setName(this.activeConversation.dialogs[this.currentIndex].responses[0].name || ig.game.playerName);
this.textBoxEntity.setText(textObj, true);
this.textBoxEntity.setActors(
this.pcAnimationSheet, this.npcAnimationSheet,
this.activeConversation.dialogs[this.currentIndex].pc_actor_frame[1],
this.activeConversation.dialogs[this.currentIndex].npc_actor_frame[1]
);
},
handleAction: function(actionObject){
if(!actionObject){
return;
}
switch(actionObject.type){
case 'goto':
// swapping to a whole new conversation
this.startConversation(actionObject.conversation || this.conversationIndex, this.conversationStore, actionObject.id);
break;
}
},
handleInput: function(){
if(!this.active){ return; }
var actionObject;
var actioned = true;
var action = null;
if( ig.input.pressed('okay') ) {
actionObject = this.disperseAction('okay');
}else if( ig.input.pressed('cancel') ) {
actionObject = this.disperseAction('cancel');
}else if( ig.input.pressed('menu') ) {
actionObject = this.disperseAction('menu');
}else if( ig.input.pressed('action') ) {
actionObject = this.disperseAction('action');
}else{
// no action this input
actioned = false;
}
if(actioned){
this.handleAction(actionObject);
}
},
disperseAction: function(key){
action = this.actions[key];
if(action){
this.resetActions();
return action();
}else if(key === "okay"){
//Okay with no action, end conversation
this.endConversation();
}
},
resetActions: function(){
this.actions.okay = null;
this.actions.menu = null;
this.actions.action = null;
this.actions.cancel = null;
},
endConversation: function(){
this.active = false;
this.textBoxEntity.kill();
this.textBoxEntity = null;
ig.game.unpause(true);
}
});
});
ig.module(
'plugins.jfry.conversation-engine.ui-text-box'
)
.requires(
'plusplus.core.config',
'plusplus.ui.ui-overlay',
'plusplus.helpers.utilsvector2',
'plusplus.helpers.utilsdraw',
'plusplus.helpers.utilsintersection'
)
.defines(function () {
"use strict";
var _c = ig.CONFIG;
var _utv2 = ig.utilsvector2;
var _utd = ig.utilsdraw;
ig.UITextBox = ig.global.UITextBox = ig.UIOverlay.extend({
displayedText: null,
fullText: null,
init: function(x, y, settings){
this.parent();
this.fullText = settings.text;
this.fill = {
width: settings.width || 800,
height: settings.height || 180
};
this.rgba = {
r: 100,
g: 100,
b: 100,
a: 0.5
};
this.isChoice = false;
this.timer = new ig.Timer()
this.pcActor = null;
this.npcActor = null;
this.pcActorIndex = 0;
this.npcActorIndex = 0;
},
update: function(){
if(!this.visible){ return; }
},
setText: function(text){
this.buttons = [];
this.isChoice = false;
this.text = "";
if(typeof text === "object"){
//build array of buttons to display and insert new lines in our text
for(var key in text){
if(text.hasOwnProperty(key)){
this.buttons.push(key);
// Using @ to denote newline
this.text = (this.text) ? this.text+" @ " +text[key]: text[key] ;
}
};
if(this.buttons.length > 1){
this.isChoice = true;
}
}else{
this.text = text;
}
this.timer.set(0);
},
setName: function(name){
this.name = name;
},
setActors: function(pc, npc, pcIndex, npcIndex){
this.pcActor = pc;
this.npcActor = npc;
this.pcActorIndex = pcIndex;
this.npcActorIndex = npcIndex;
},
drawText: function(ctx, x, y, maxWidth, lineHeight){
var words = this.text.split(' ');
var line = '';
var limited = null;
var position = parseInt(this.timer.delta()*20, 10);
var choiceIndex = 0;
for(var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var testWidth = ig.game.font.widthForString(testLine);
if (testWidth > maxWidth && n > 0 || words[n] === "@") {
limited = ig.game.font.draw(
line, x, y,
ig.Font.ALIGN.LEFT,
position
);
if(words[n] === "@"){
line = '';
this.drawChoiceButton(x, y, this.buttons[choiceIndex++]);
}else{
line = words[n] + ' ';
}
y += lineHeight;
position = position-limited;
} else {
line = testLine;
}
if(limited === "limit"){
return;
}
}
ig.game.font.draw(line, x, y, ig.Font.ALIGN.LEFT, position);
if(choiceIndex){
this.drawChoiceButton(x, y, this.buttons[choiceIndex++]);
}
},
drawContinueButton: function(ctx, x, y){
if(this.isChoice){ return; }
var anim = ig.game.buttonMap.okay;
//anim.update();
anim.draw( x-135,y-58);
ig.game.smallFont.draw("Continue", x-103, y-55);
},
drawChoiceButton: function(x, y, button){
var anim = ig.game.buttonMap[button];
// anim.update();
anim.draw( x-40,y+3);
},
drawName: function(x, y){
ig.game.font.draw(this.name+":", x, y, ig.Font.ALIGN.LEFT);
},
drawActors: function(left, top, right){
this.pcActor.drawTile(left, top-288, this.pcActorIndex, 288);
this.npcActor.drawTile(right-220, top-288, this.npcActorIndex, 288);
},
draw: function(){
if(this.visible){
var ctx = ig.system.context;
var top = ig.system.height-this.fill.height;
var left = ig.system.width/2-this.fill.width/2;
var grd = ctx.createLinearGradient(0,top,0,top+75);
var rect = ctx.roundRect(
left, top-18, this.fill.width, this.fill.height, 10
);
this.drawActors(left, top, this.fill.width);
// Blue gradient
grd.addColorStop(0,"#4748fa");
grd.addColorStop(1,"#0a08ef");
ctx.fillStyle=grd;
// Beveled Stroke
rect.fill();
ctx.lineWidth = 5;
ctx.strokeStyle = 'white';
rect.stroke();
ctx.lineWidth = 3;
ctx.strokeStyle = '#eeeeee';
rect.stroke();
ctx.lineWidth = 1;
ctx.strokeStyle = '#dddddd';
rect.stroke();
if(this.isChoice){
this.drawText(ctx, left+70, top+20, this.fill.width-90, 30);
}else{
this.drawText(ctx, left+20, top+20, this.fill.width-40, 30);
}
this.drawName(left+15, top-17);
this.drawContinueButton(ctx, left+this.fill.width, top+this.fill.height);
}
}
});
});