To test the theory of creating a random plot we need to build some type of mini-program or framework. As I said before it's very hard to seperate plot from game engine so this might be far from what you envision as ideal.

We need at least some type of dummy game engine or dummy classes to demonstrate a running plot. We can use interfaces in an attempt to make it somewhat more modular. By creating something simple we can test out ideas and see if they're feasible and hack out rough scripts.

Here I'm going to use C# and Lua. C# is wonderful and can be used to create games with ease and efficency. Lua is extremely simple to embed and is widely used.

As you should know C# doesn't support multiple inhertitance but it does support multiple interfaces! So potentially ... (You'd be very lucky) ... you can take the code we write, implement the correct interfaces and it will magically work in your game. Yay!

A Framework

We're going to build the framework around the "There are rats in my basement quest", so we need objects like basements and rats (see how hard it is to create a completely seperate-from-a-game-engine program!).

The framework can be thought to represent the game world. Then we'll add a lua interface and finally a lua script. We can't do anything without the framework though.

To start we'll create a simple Lua enabled program. Please read my tutorials on this if you don't know where to start. Here's some beginning code that compiles:

C#:
  1. using System;
  2. using LuaInterface;
  3.  
  4. namespace PlotFrameWork
  5. {
  6.     ///
  7.     /// Summary description for Class1.
  8.     ///
  9.     class Class1
  10.     {
  11.         static Lua lua = new Lua();
  12.         ///
  13.         /// The main entry point for the application.
  14.         ///
  15.         [STAThread]
  16.         static void Main(string[] args)
  17.         {
  18.             Console.WriteLine("Plot Framework");
  19.             //magic goes here
  20.             Console.ReadLine();
  21.         }
  22.     }
  23. }

This is where we're going to start. I'm going to try and keep these brief which means it won't be as clean or as expandable as it could be and it's going to be reasonably tailored to our single rat-in-the-basement-quest.

So what's the very minimum we need?

      One NPC
      One Cellar
      One small creature
      One reward

Still a lot of work!

Let's create everything Lua's going to use in it's own namespace. This is important for how we register classes with Lua.

C#:
  1. namespace LuaPlots
  2. {
  3. }

Just waiting to be filled with the objects and actors and plots that we'll weave into an epic story. For now we're going to have one room of one type - the cellar.

From this we'll create one NPC who owns said cellar. When it comes round to harvesting he'll immediately be found to be compatible, and being the only NPC he'll be selected. We're going to start with a class that might come in handy (bottom up programmnig style). The name class. It contains a name and it's plural, useful for conversations to prevent things like "There are mouses in my cellar."

For some reason with all the getters and setters it's turned out massive. We can expand this so we know whether to use "a" or "an" or "the" or "they" and all that. We don't want sentences like "Here's your reward its a gems!".

C#:
  1. public Name(string singular, string plural)
  2. {
  3.     this.singular = singular;
  4.     this.plural = plural;
  5. }
  6.  
  7. private readonly string singular;
  8. private readonly string plural;
  9.  
  10. public string Singular
  11. {
  12.     get { return singular;  }
  13. }
  14.  
  15. public string Plural
  16. {
  17.     get { return plural;  }
  18. }
  19.  
  20. public override string ToString()
  21. {
  22.     return singular;
  23. }

(As an aside I was recently reading the Angbang source and there's a function in there that can change most singluars to plurals with out a problem. It may be worth using something similar to that code)

Now we can name things quite well - why not build some things to actually name? Let's start with a room and - we'll describe what we need from a room object with an interface.

C#:
  1. public interface IRoom
  2. {
  3.  
  4.     string Type
  5.     {
  6.         get;
  7.         set;
  8.     }
  9.  
  10.     Name Name
  11.     {
  12.         get;
  13.         set;
  14.     }
  15.  
  16.     void AddMonsters(IMonster[] m);
  17. }

The interface specifies how we intend to use a room object. The next interface we want is that of an NPC. This is the person who's going to own the room. Again an NPC should have a name - and this time a method for discovering what rooms they own.

C#:
  1. public interface INpc
  2. {
  3.     Name Name
  4.     {
  5.         get;
  6.         set;
  7.     }
  8.  
  9.     IRoom FindOwnedRoom(string type);
  10. }

There are a few more interfaces to go. One is the monster interface the other is the world interface. Let's do the monster one first.

C#:
  1. public interface IMonster
  2. {
  3.     Name Name
  4.     {
  5.         get;
  6.         set;
  7.     }
  8.  
  9.     void Kill();
  10.  
  11. }

Very simple it has a name and a function where you kill it. What more could you want from a monster? Last interface is the gameworld this will store the C sharp classes and have few functions that are used directly with Lua.

C#:
  1. public interface IGameWorld
  2. {
  3.     INpc[] AllNpcs();
  4.  
  5.     IMonster[] GetSmallMonsters();
  6.     string GetSmallReward();
  7. }

That's the game world nice and simple. Notice how we're just using text for rewards. There's one last layer of detail required - delegates / callbacks. They're very important to create smooth running quests.

The types of delegates you'll want to use though are definetly game engine related. Some are pretty general like things dying. But getting super-secret-power-Z by eating a PowerStone is a feature that's only ever going to happen in your own engine.

There are two events that concern us for our quest. Conversation events and Death events.

Delegates are great and are supposedly supported by Lua but I could not get them to work in the documented way. Supposedly you define a delegate.

C#:
  1. public delegate  void Death(string howDeathHappened);

Fair enough. Then if you make events in your interface like so:

C#:
  1. public event Death OnDeath

Well then in Lua you should be able to add to the event by using the code.

Lua:
  1. someObj.OnDeath:Add(luaFunctionThatMatchesDelegate);
  2.  
  3. function luaFunctionThatMatchesDelegate(howDied)
  4. --stuff
  5. end

Well this told me Add didn't exist. When I messed things around so that it supposedly did exist (by explicitly defining Add and Remove in C#) it crashed with a StackOverFlow error.) To say the least this was both frustrating and upsetting. After a couple of failed hackish work arounds I came up with a reasonably painless hack to do the job.

If you don't experience the above problems then you can do it the *correct* way but I somehow doubt it's just me alone on my computer.

Well let's begin the Voodoo and get death in their. When a rat dies we're going to give it a death event. We'll start by defining death events in their most general form - a delegate.

C#:
  1. namespace LuaPlots
  2. {
  3.     public delegate void Death(string how);

Right at the top. So a death event comes with a string describing how the death occured - this could easily be extended to class with lots of details. Imagine writing plots about posion or multi-murders and you may start to feel you want detailed death info.

We want all monsters to be able to die. So we throw in a death event. In the monster interface.

C#:
  1. event Death OnDeath;
  2.  
  3.     Death AddDeathEvent
  4.     {
  5.         set;
  6.     }
  7.  
  8.     Death RemoveDeathEvent
  9.     {
  10.         set;
  11.     }

So there's the event and oh look two oddly named functions - could this be the nasty hack? Why yes, yes it is! By assigning delegates to those to set functions we'll add and remove Death Events. Doesn't quite seem to make sense at this stage and you can ignore it for now. I'll explain the details when we implement a monster and write Lua code.

The next one is a talking event. If you have a full conversation engine you can add this in yourself. Here it will be called everytime the NPC speaks.

Once again we start with delegate.

C#:
  1. namespace LuaPlots
  2. {
  3.     public delegate void Death(string how)//used for when something dies
  4.     public delegate void Speak(string speech); //used for when something speaks

Look at those comments! Almost looks like someone knows how to program. Quite similar to death but this time we're going to add it to the INpc interface. So NPCs can talk and Monsters can die - that's how you make games. Here's the code to give NPCs speech.

                event Speak OnSpeak;

  Speak AddSpeakEvent
  {
   set;
  }

  Speak RemoveSpeakEvent
  {
   set;
  }

There's the code so we can add Lua functions as reponses to C# events such as death. Say if a rat dies then the Lua function OnARatDying can be called. Yes, yes a little bit cryptic but you'll be glad to hear thats the end of the top-level framework. The next bit is to create objects for all these lovely interfaces. First the world!

public class TestWorld : IGameWorld
{

INpc[] npcList;
IRoom[] roomList;

public TestWorld()
{
roomList =
new IRoom[]
{
new NormalRoom(new Name("cellar", "cellars"), "storage")
};

npcList = new INpc[]
{
new NPC(new Name("Pete","Petes"), roomList[0])

};
}
public INpc[] AllNpcs()
{
return npcList;
}

public IMonster[] GetSmallMonsters()
{
return new Rat[] {new Rat(), new Rat() };
}

public string GetSmallReward()
{
return "10 pieces of Gold";
}
}

Thats the world. It's assumed to come with a few standard generation and search functions. So we can search for monsters, objects, npcs and places according to various specifications. For instance "Find a man who owns a pet rat". In the "TestWorld" I've merely added the basics required for one single rat-hunting quest. It's easy to expand if we want to test out different quests (the code will probably need changing a little in some places :D)

In our idealized world we have an NPC list of everyone who lives in the world. And perhaps slightly oddly a list of all the different rooms that exist in the world.

In the constructor we create one NPC who owns one room - a cellar. This is not a very populated world. Then we have functions to get all the npcs to get all the rooms and generator functions to generate a group of small monsters and also a small treasure.

Notice that we're only generating one type of monster and one reward. This is called laziness - also it's only a framework! Feel free to expand upon this.

Next up we'll create a room. We're going to call this room object "Normal Room".

public class NormalRoom : IRoom
{
private string type;
private Name name;
private IMonster[] monsters;

public IMonster[] Monsters
{
get { return monsters; }
set { monsters = value; }
}

public Name Name
{
get { return name; }
set { name = value; }
}

public string Type
{
get { return type; }
set { type = value; }
}

public NormalRoom(Name roomName, string roomType)
{
name = roomName;
type = roomType;
}

public void AddMonsters(IMonster[] m)
{
monsters = m;
}

}

Notable things here include the AddMonsters function. This is quite specific to our needs. In a real game there might be an Add Entity function. Or possibly you'd get the room co-ordinates and then throw the rats in. You might also require some elaborate means to keep the rats in the room. These are all problems we don't care about for this demonstration.

So basically rooms can store monsters and you can query a rooms type. Next up let's add the monster. Now here things are a little different because of the event.

public class Rat : IMonster
{
static Name name = new Name("rat", "rodents");

//private event Death onDeath;
public event Death OnDeath;

public Rat()
{

}

#region IMonster Members

public Name Name
{
get
{
return name;
}
set
{
name =
value;
}
}

public void Kill()
{
// TODO: Add Rat.Kill implementation
this.OnDeath("How? Well it was killed by a player.");
}

//This is mangled C# code so it can interface with Lua
public Death AddDeathEvent
{
set{ OnDeath += value;}
}

//Same as AddDeathEvent
public Death RemoveDeathEvent
{
set{ OnDeath -= value; }
}

#endregion

}

You can see the round about way we're modifying delegates here. I'm still not all together sure if the remove one even works! But onwards and upwards. Important to thing to note here is that when the rat has it's Kill() function called it in turn activates it's Death event. We can hook this event up to our scripts no problem (well there where are few problems getting Lua to play well with the delegates but it's okay now)!

So we have a room, a world, a suitably small enemy - now we need the NPC. Here's the code.

public class NPC : INpc
{
Name name;
IRoom ownedRoom;
String conversation = "I have nothing to say";

public NPC(Name NPCname, IRoom room)
{
name = NPCname;
ownedRoom = room;
}

public event Speak OnSpeak;

public void Spoke()
{
if(OnSpeak != null)
OnSpeak("Spoke");
}

public Speak AddSpeakEvent
{
set { OnSpeak += value; }
}

public Speak RemoveSpeakEvent
{
set { OnSpeak -= value; }
}

#region INpc Members

public Name Name
{
get
{
return name;
}
set
{
name =
value;
}
}

public string Conversation
{
get { return conversation; }
set { conversation = value; }
}

//If it can't find a room null is returned
public IRoom FindOwnedRoom(string type)
{
if(ownedRoom.Type == type)
return ownedRoom;
else
return null;
}

#endregion

}

NPCs can own things and them talking is an event. Firing this event with the current conversation system is very hard. (we don't want to fire when the conversation string is accessed we want to fire after it. Because if we had code:


fireTalkEvent
return talkObject;

Well the event might change the talk object and all sort of problems would occur.)

Therefore I've added a extra function Spoke we'll manually call this after the NPC says anything. Bit hackish but this isn't the most ideal conversation system really.

That's all the objects we need!

7.2 A user interface for the Framework

I promise soon we'll get to Lua and the script but first we need some way to interact with the world or we won't be able to test our script. We're using a console program so all interaction will be done through ReadLine yay like a very simple text game.

The plan is simple we, the player, will see the one NPC and we'll have T for Talk and C for go to cellar. Then from the cellar we can return to the NPC. The cellar will show it's contents to us. If there's something there we can kill then we can press the K key to kill. Truely the finest in interactive technology. So here we go let's create a simple game.

This is the least essential bit so I'm going to be throwing the code down in two big chunks. One for each "room" though one "room" can be thought to be the void as we're not going to have a room object there.

class Class1
 {
  static Lua lua = new Lua();
  static TestWorld testWorld = new TestWorld();

Here a Lua interface is created, as is a world object. In the constructor we want to add the NPC to the world and the cellar.

 //The Start Point of The User Interface
  public void UIStart()
  {
   string answer = "";
   while(answer.ToLower() != "t" && answer.ToLower() != "c"
    && answer.ToLower() != "q")
   {
    Console.WriteLine("\t\t---PLOTS GENERATION TEST------\n");
    Console.WriteLine("Test World Has Only One Occupant");
    Console.WriteLine("Here stands " + testWorld.AllNpcs()[0].Name.Singular);
    Console.WriteLine("\tPress \"T\" to talk to " + testWorld.AllNpcs()[0].Name.Singular);
    Console.WriteLine("\r\tPress \"C\" to go to the cellar the only room in existance");
    Console.WriteLine("\r\tPress \"Q\" to quit");
    answer = Console.ReadLine();

   }

   if(answer.ToLower() == "t")
   {
    Console.WriteLine(testWorld.AllNpcs()[0].Name + " says \"" +
     testWorld.AllNpcs()[0].Conversation + "\"");
    ((NPC)testWorld.AllNpcs()[0]).Spoke();
    UIStart();
   }

   if(answer.ToLower() == "c")
   {
    UIRoom(testWorld.AllNpcs()[0].FindOwnedRoom("storage"));
   }
  }

It's very simple if you press a wrong key then the whole thing will just be reshown. Notable we cast the INpc interface to NPC so we can call the Spoke method. The second rooms a little more complicated. We want to alter the description based on the contents are

  • no monsters
  • one monster
  • multiple monsters

Here it is in all it's glory.

public void UIRoom(IRoom r)
{
 NormalRoom n = r as NormalRoom;
 if(r != null)
 {

  string answer = "";
  while(answer.ToLower() != "x" && answer.ToLower() != "k")
  {

   if(n.Monsters == null)
   {
    Console.WriteLine("An empty " + n.Name);
   }
   else
   {
    if(n.Monsters.Length == 0)
     Console.WriteLine("An empty " + n.Name);
    else
    {
     if(n.Monsters.Length == 1)
     {
      Console.WriteLine("There is one " + n.Monsters[0].Name.Singular + " here!");
     }
     else
     {
      Console.WriteLine("There are " + n.Monsters[0].Name.Plural + " here!");
     }

     Console.WriteLine("Press K to kill a " + n.Monsters[0].Name.Singular);
    }

   }
   Console.WriteLine("Press x to exit");
   answer = Console.ReadLine();
  }

  if(answer == "x")
   UIStart();

  if(answer == "k")
  {
   if(n.Monsters.Length == 0)
   {
    Console.WriteLine("There's nothing to kill.");
    UIRoom(r);
    return;

   }

   n.Monsters[n.Monsters.Length-1].Kill();

   //Reduce the array by one
   IMonster[] newArray = new IMonster[n.Monsters.Length-1];
   for(int i = 0; i < newArray.Length; i++)
   {
    newArray[i] = n.Monsters[1];
   }
   n.Monsters = newArray;
   UIRoom(r);
  }

 }
}

The recursion bugs me a bit - if you were willing you could cause a stack overflow by always pressing k. It would take a long long time though. Anyway that's the code. Press K and the first monster in the room is killed. The rooms monster array is then taken and reduced by one.

That's the user interface done. Next up is hooking up Lua and the game script we'll be having. We can acutally play "the game" without the script. All we'll see is a single NPC and be able to move to the cellar and back to the NPC.

7.3 Registering Lua

Let's check out the main function. We want to register a few helper classes with Lua. We also want to run the quest we've created.

static void Main(string[] args)
  {
   Class1 c = new Class1();

   c.RegisterGameFrameWork();
   Console.WriteLine("Plot Framework");
   Console.WriteLine("Calling Lua File");

   try
   {
    lua.DoFile("infest.txt");
   }
   catch(Exception error)
   {
    Console.WriteLine("Lua Problem: " + error.Message.ToString());
   }

   c.UIStart();
   Console.WriteLine("Finished Calling Lua File");
  }

Yeah I've called the script "infest.txt". You can comment that line out and the c.RegisterGameFrameWork(); and it should work. So what's in the RegisterGameFrameWork? Let's fine out!

public void RegisterGameFrameWork()
  {
   lua.OpenBaseLib();
   lua.OpenMathLib();
   lua.OpenTableLib();

   lua.RegisterFunction("GetNPCs",testWorld,testWorld.GetType().GetMethod("AllNpcs"));
   lua.RegisterFunction("GetSmallReward", testWorld, testWorld.GetType().GetMethod("GetSmallReward"));
   lua.RegisterFunction("GetSmallMonsters", testWorld, testWorld.GetType().GetMethod("GetSmallMonsters"));

   lua.RegisterFunction("Output", this, this.GetType().GetMethod("Output"));
   lua.RegisterFunction("ID", this, this.GetType().GetMethod("ID"));
  }

Pretty straight forward. You might be curious about the Output and ID functions. These are debug functions that I use for Lua. Here's the code for them.


 public void ID(Object o)
  {
   if(o == null)
    Console.WriteLine("Null value");
   else
   {
    Console.WriteLine(o.ToString());
   }
  }

  public void Output(string output)
  {
   Console.WriteLine(output);
  }

And that's it Lua is linked up. We've registered the important functions. Everything should work great! The last thing is the script itself.

7.4 Our quest script

All scripts need to be aware of certain C# classes. To become aware of C# classes a few lines of code need writing. We should create a small startup to be run once at the start of the game that would add all required classes. But, because we only have a single script I'm just including that registration info directly.

---
-- Basic Setup Code
-- This would probably be done once somewhere else and only ever done once.
---

load_assembly("LuaPlots");
IRoom = import_type("LuaPlots.IRoom"); -- import the room interface
INpc = import_type("LuaPlots.INpc"); -- import npc interface

I've split the script up into Resources there are some resources we seed :- reward for example and the conversations and there are some we harvest the npc and the room. The harvesting, seeding process I called Farming and we use a farm function.

So we have some resources. I put a lot of these together in table that sits in the script and can be accessed from anywhere - they're global.

The resource table also includes information on the current state of the quest. This includes on number called questState, telling us what state we're at and another counter telling us how many monsters remain. Here it is:

---
-- The Farm Function (it harvests and seeds)
---

function farm()
 -- Get The NPC
 Resources.npc = GetNPCs();
 Resources.npc = Resources.npc[0];

 -- Get The Room Where The Monsters Are
 Resources.room = Resources.npc:FindOwnedRoom("storage");

 -- Get some monsters
 Resources.monsters = GetSmallMonsters();

 -- Get a small reward
 Resources.reward = "10gp";

 ---
 --Link Everything Up
 ---

 --Record The Number of Monsters
 Resources.monsterCurrent = Resources.monsters.Length;

 --Put the monsters in the cellar (could do this AFTER accepting quest)
 Resources.room:AddMonsters(Resources.monsters);

 --Put reward on NPC (I'm not going to do this but just mentioning that we could)

 -- Fill out the non-harvest stuff
 SetStartConversation();

 ---
 -- Hook Up Quest Events
 ---
 for i = 0, Resources.monsters.Length-1 do
  Resources.monsters[i].AddDeathEvent = monsterKilled;
 end

 Resources.npc.AddSpeakEvent = onNPCSpeak;

end

Cool yeah! The new code at the bottom is attaching a lua function to each and every monster's Death event. Also the npc gets an event attached to it's Speak event.

A function is also called to assign conversation to the NPC. So we need to look at the event handling functions and the conversation writing functions. Here one of the conversation functions.

function SetStartConversation()

 IHaveMonsters = "You seem to be a brave adventurer. It seems my "
   .. Resources.room.Name.singular .. " "
   .. "has been infested by ";

 if Resources.monsters.Length == 1 then
  IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Singular;
 else
  IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Plural;
 end

 IHaveMonsters = IHaveMonsters .. "\n\rPlease kill them, there's a good a chap.";

 Resources.npc.Conversation = IHaveMonsters;
end

Okay here is the first thing the NPC will say to you if you talk to him before killing all his rats.

If he only has one creature he'll refer to it in the singular otherwise he'll use the plura. In our case he refers to it in plural terms. The script generates the conversation and then assigns the conversation to the NPC. That's it. All the other conversations work the same way.

Now let's look at how we handle an event. I'm going to fill in what happens when you talk to the NPC, because it's simpler.

function onNPCSpeak(speech)

 if(Resources.questState == 3 or Resources.questState == 2 ) then
  Resources.questState = 4;
  Resources.npc.Conversation = "I've nothing to say";
  Resources.npc.RemoveSpeakEvent = onNPCSpeak;
 end

 if(Resources.questState == 0) then
  Resources.questState = 1;
  SetMidConversation();
 end

end

If you've never talked to him before (and haven't killed all his rats) then the quest auto-intiates. Then if you go kill all his rats and talk to to him then again - the quest is set to over. On the quest ending we also set the conversation string to something else so next time we talk to him he'll say this. That way we don't have a random NPC constantly thanking us for saving his basement from rats. Finally we unregister the event. There should be more clean up stuff here to. If we used a lua table to contain the entire quest (like a namespace in C#) we could now set it to nil.

When all the rats die. Then we set the conversation to some kind of thank you speech depending how you proceeded through the quest. Did you kill the rats before you were asked too and so on.

Okay here's the entire script. It should be pretty self explantory (i guess :D)

Lua:
  1. --[[
  2. Plot Name:  Remove an Infestation
  3. Author:  Daniel Schuller
  4. Version:  1
  5. Rundown
  6. =======
  7. Some small creature have infested some one's store room.
  8. They'd like you to clean it.
  9. We don't consider duration. His 212th son is going to have the same problem if you don't help.
  10. (all quests should have some resolution that doesn't involve the player)
  11. Seed Stage: Monsters are planted as is conversation. Conversation is not as easy to edit as it could be.
  12. --]]
  13. ---
  14. -- Basic Setup Code
  15. -- This would probably be done once somewhere else and only ever done once.
  16. ---
  17.  
  18. load_assembly("LuaPlots");
  19. IRoom = import_type("LuaPlots.IRoom"); -- import the room interface
  20. INpc = import_type("LuaPlots.INpc"); -- import npc interface
  21.  
  22. ---
  23. -- Resources
  24. ---
  25.  
  26. Resources =
  27. {
  28.     npc,
  29.     room,
  30.     monsters,
  31.     reward,
  32.  
  33.      ---
  34.      --PlotCounters
  35.      ---
  36.  
  37.     monsterCurrent, --no of monster left alive
  38.  
  39.     questState = 0
  40.   --[[
  41.    0: Not Talked To NPC
  42.    1: OnQuest
  43.    2: RatsDead + OnQuest
  44.    3: Rats Dead Not On quest
  45.    4: Rewarded Quest Over
  46.   --]]
  47. }
  48.  
  49. ---
  50. -- *Functions*
  51. ---
  52.  
  53. ---
  54. -- Conversation Setters
  55. ---
  56.  
  57. function SetStartConversation()
  58.  
  59.     IHaveMonsters = "You seem to be a brave adventurer. It seems my "
  60.     .. Resources.room.Name.singular .. " "
  61.     .. "has been infested by ";
  62.  
  63.     if Resources.monsters.Length == 1 then
  64.         IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Singular;
  65.     else
  66.         IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Plural;
  67.     end
  68.  
  69.     IHaveMonsters = IHaveMonsters .. "\n\rPlease kill them, there's a good a chap.";
  70.  
  71.     Resources.npc.Conversation = IHaveMonsters;
  72. end
  73.  
  74. function SetMidConversation()
  75.  
  76.     IStillHaveMonsters = "Why are talking to me? I thought you were cleaning out my "
  77.     .. Resources.room.Name.Singular .. ". Well get to it!"
  78.  
  79.     Resources.npc.Conversation = IStillHaveMonsters;
  80. end
  81.  
  82. function SetEndConversationOne()
  83.  
  84.     Thanks = "Thanks for solving my problem. Here's a little something for you trouble. It's "
  85.     .. Resources.reward;
  86.     Resources.npc.Conversation = Thanks;
  87. end
  88.  
  89. function SetEndConversationTwo()
  90.  
  91.     ThanksIGuess = "Oh hello. You're the chap who was making all the noise in the cellar, what?"
  92.     .. "\n\r Well I did find those rats a damn nusiance, so I'd like to compensate you!" ..
  93.     "\n\rEven if you did come into my private property and enter my rooms unannounced!" ..
  94.     "Here's a " .. Resources.reward .. ". Enjoy!";
  95.  
  96.     Resources.npc.Conversation = ThanksIGuess;
  97. end
  98.  
  99. ---
  100. --Event Handlers
  101. ---
  102.  
  103. function onNPCSpeak(speech)
  104.  
  105.     if(Resources.questState == 3 or Resources.questState == 2 ) then
  106.         Resources.questState = 4;
  107.         Resources.npc.Conversation = "I've nothing to say";
  108.         Resources.npc.RemoveSpeakEvent = onNPCSpeak;
  109.     end
  110.    
  111.     if(Resources.questState == 0) then
  112.         Resources.questState = 1;
  113.         SetMidConversation();
  114.     end
  115.  
  116. end
  117.  
  118.  
  119. function monsterKilled(how)
  120.  
  121.     monsterPlural = Resources.monsters[0].Name.Plural;
  122.  
  123.     if(Resources.questState == 1) then
  124.         Output("There's nothing more fun than killing " .. monsterPlural  ..
  125.         " for " .. Resources.npc.Name.Singular);
  126.     end
  127.  
  128.     Resources.monsterCurrent = Resources.monsterCurrent - 1;
  129.  
  130.     if(Resources.monsterCurrent == 0) then
  131.  
  132.         --Advance the quest
  133.         if(Resources.questState == 0) then
  134.             Resources.questState = 3;
  135.             SetEndConversationTwo();
  136.         elseif(Resources.questState == 1) then
  137.             Output("Old big nose should be happy, that's all his " .. monsterPlural ..
  138.             " killed!");
  139.             Resources.questState = 2;
  140.             SetEndConversationOne();
  141.         else
  142.             Output("There's needs to be a serious bug to get here. Quest state is "
  143.             .. Resources.questState);
  144.         end
  145.     end
  146. end
  147.  
  148. ---
  149. -- The Farm Function (it harvests and seeds)
  150. ---
  151.  
  152. function farm()
  153.  -- Get The NPC
  154.     Resources.npc = GetNPCs();
  155.     Resources.npc = Resources.npc[0];
  156.  
  157.  -- Get The Room Where The Monsters Are
  158.     Resources.room = Resources.npc:FindOwnedRoom("storage");
  159.  
  160.  -- Get some monsters
  161.     Resources.monsters = GetSmallMonsters();
  162.  
  163.  -- Get a small reward
  164.     Resources.reward = "10gp";
  165.  
  166.  ---
  167.  --Link Everything Up
  168.  ---
  169.  
  170.  --Record The Number of Monsters
  171.     Resources.monsterCurrent = Resources.monsters.Length;
  172.  
  173.  --Put the monsters in the cellar (could do this AFTER accepting quest)
  174.     Resources.room:AddMonsters(Resources.monsters);
  175.  
  176.  --Put reward on NPC (I'm not going to do this but just mentioning that we could)
  177.  
  178.  -- Fill out the non-harvest stuff
  179.      SetStartConversation();
  180.  
  181.  ---
  182.  -- Hook Up Quest Events
  183.  ---
  184.     for i = 0, Resources.monsters.Length-1 do
  185.         Resources.monsters[i].AddDeathEvent = monsterKilled;
  186.     end
  187.  
  188.     Resources.npc.AddSpeakEvent = onNPCSpeak;
  189. end
  190.  
  191. --This is the actually running code! :o
  192. farm();

8. Final Words

Well there it is. A nice reusable random plot thingy, just what every game needs. The next thing you should probably try is creating a new plot here are some ideas:

  • I seem to have lost my [small precious item] could you find it?
  • You must prove yourself to me by [doing small task] before i'll give you [reward]
  • Please kill the person sleeping with my siginifcant other

etc etc The plots can be quite complicated and multistaged.
Try and implement a story arc. Consider how to save plots mid way through! We'd possibly want to store the Resource table is there some way to do this without writing it into the plot file?

There are plenty of interesting problems to tackle! Good luck tell me what you make!

(Saving wise you'd want to seperate out your event linking and then save the resource block using hash codes for the big objects. Then load the resource block and call the link function. The link function should be error tolerant. i.e. if there are monsters and they're dead they're going to be nill values don't try and link events in!)

Speaking of which, it can't happen in our game but what if the quest giver dies. These things should be accounted for.

Source Code

Random Lua Plots.zip

Further Reading

Little, as far as I can find, has been written on plot generation. (outside hard to access academic papers! Google Scholar is a step in the right direction though.)

Technorati Tags: , , ,


1 Response to “Coding self contained reusable plots”

  1. 1 Procedural Content Generation: Articles

Currently browsing

Currently Reading

  • Large-Scale C   Software Design

    Large-Scale C Software Design by John Lakos

  • I Am a Strange Loop

    I Am a Strange Loop by Douglas Hofstadter

  • A Dictionary of Basic Japanese Sentence Patterns (Kodansha Dictionary)

    A Dictionary of Basic Japanese Sentence Patterns (Kodansha Dictionary) by Naoko Chino

  • Japanese Verbs at a Glance (Power Japanese Series) (Kodansha\’s Children\’s Classics)

    Japanese Verbs at a Glance (Power Japanese Series) (Kodansha\’s Children\’s Classics) by Naoko Chino

Goto Library