Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by airesso

Hi everyone. I'm working on a game that will have many dialog options and end up setting game flags later on based on choices. I've been playing with different ways to do this and have come up with the following. Wanted to see if anyone had suggestions.

Basically each character will have a js object associated to their dialog that will get added to a larger conversationStore object. Then conversations can be looked up through zone_entityID lookup. From there a getConversationIndex (unique to that zone/character) will check some flags to determine the current dialog item that the conversation is on and show it accordingly. Let me know what you think.

{
	// Holds the arc for a specific character
	home_zone_01: {
		conversations: [
			{
				text: "Hello There",
				npc_avatar: [ "character-1-happy" ],
				// Index 0 is when the conversation first load
				// Index 1 is for during the response
				pc_avatar: [
					"player-neutral",
					"player-confused"
				],
				// If there are 2 or more responses it's presented as a choice to the player
				responses: [
					{
						text: "Umm... Me?",
						action: function(){
							return "goto:dialog#1";
						}
					},
					{
						text: "Hi!",
						action: function(){
							// Return value is parsed and determined to be another dialog. Load dialog index 2
							return "goto:dialog#2";
						}
					}
				]
			},
			{
				text: "Yes you!",
				npc_avatar: "character-1-happy",
				pc_avatar: [
					"player-confused",
					"player-happy"
				],
				responses: [
					// Single Response doesn't have a selection option
					{
						text: "Nice to meet you!",
						// This executes when the dialog window closes. Changes flags
						action: function(){
							this.conversationFlags.update("home_zone_01", 3);
							// No return value so conversation ends
						}
					}
				]
				// If responses is null it would check for an action to execute. If no action then the dialog would close.
			}
		],
		getConversationIndex: function(){
			// Checks the tags and returns an int for the conversation
			return 0;
		}
	}

}

It feels a little lengthy for a single conversation but I wanted it to have many features including: pc and npc avatar, branching options, ability to update game flags, option to jump around to previous conversation items.

Let me know what you think.

1 decade ago by drhayes

Seems useful to me! I'm assuming you've mapped conversations in your game and haven't run into any roadblocks..?

Thing I like about this is it's graphics agnostic. You could write a command-line utility to test these/interact with them and it'd work just fine. Real strong point in its favor.

Don&039;t let me over-engineer for you... but have you thought about longer text snippets and how they break up per page of text? Like, do you want to be very explicit about that in your #text properties and have an array of lines, each no longer than some number of characters? Or are you going to let the graphics figure it out?

1 decade ago by airesso

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);
			}
		}
	});
});

1 decade ago by airesso

Oh also a conversation is launched with something like:
ig.game.conversation.startConversation('home_zone_01', conversations);
Page 1 of 1
« first « previous next › last »