Fortress Occident Developer Blog

Comments

THE DESIGN ETHOS OF OUR ROLE PLAYING SYSTEM

I’ve spent most of my adult life tinkering on this system. (Most sounded better than all.) It used to be huge and unwieldy, meant to span several books. Originally we wanted to build a set of tabletop rulebooks inseparable from it’s setting. An end-all, include-all sourcebook / setting for tabletop role playing. Over the years we’ve cut it down to fit on a napkin.

For “No Truce With The Furies” we adapted those same pen and paper rules for an isometric RPG. I firmly believe it was the right choice.

The years have seen a lot of critique of our own system and even more learning from others. We’ve followed the debates around J. Sawyer’s design for Pillars of Eternity, rated editions of D&D and even played Temple of Elemental Evil for it’s turn based engine. We’ve done our time, so to say. Today I want to share some of our main beliefs as designers of role playing systems. These are not the “pillars of our design”, just some reasoning behind our rule system. The list is meant to show where we’re coming from to fellow designers and RPG enthusiasts.

Concept art for our skill icons

1)  Only one

We make one system, one world – and that’s it. Everything goes into this one structure. All our ability for systematic thinking, all our knowledge of history. We iterate upon these rules until they are just right, the best numerical foundation for experiencing our world. And we make the world as complete and total as we can. And then we’re done. Wrap it up and send it into the future, New Testament style. We will never make a steampunk cyberpunk spyworld, Owls versus Foxes, Yarn Boy rule system.

2)  Tyranny of cool

If a skill has a great name, if a talent is poetic, if a mechanic is haute tension – it’s in. We’ll make it work. Beautiful stuff does not get taken out because “someone somewhere” didn’t understand what it does. If it’s clunky or extraneous we iterate and redesign until it works. We will always have talent names longer than “Small Black Flowers That Grow In The Sky” and “The Intense Humming of Evil” combined.

3)  Unsymmetrical is symmetrical

It’s good to have almost useless things and seemingly overpowered things. A good composition is not all equal parts. A good composition is equal experiences. There is great symmetrical tension and effect in a seemingly useless abilities that you try to use for the sake of cool. Pull off Spook, Shocking Grasp and Spell Thrust in Baldur’s Gate and you’re a wizard. All builds should not be viable, but all builds should be interesting. Some skills only pop up one or two times — they will be all the more special for it. While other’s buzz around as often as possible. (Empathy always tells you what people are feeling but when Shivers comes in, it’s a special moment.)

4)  Fit on a napkin or fit in the trash bin

After a while, we want you to be able to draw the entire system on a napkin from your head. That’s how elegant and self contained we want it to be. There are four stats and everything folds back into their value. We only use six sided dice. We prefer the Babylonian system of sixes to the Roman system of tens. (Six is a more comprehensible number, ten is too vague and philosophical and includes a zero). If we have a number in the rules – 4, 3 or 6 – we will reuse it as often as possible. All numbers fold back into themselves, everything is it’s own cap, never multiply, never produce long formulas.

5)  Small numbers

Congratulations, you just got +1 of something. It’s a big deal. Six is the maximum. You don’t get 28 experience, you get ONE POINT to put into a skill. That one point gives you the aforementioned +1 bonus. You don’t suffer 76 damage, you lose TWO LIVES. The smaller a number, the less you have of it, the more dramatic it will feel. We large mammals have two to three offspring. We have one home. We have two eyes. Our numerical values are large and chunky, losing one is tragic and gaining one is a triumph. Our system reflects that.

6)  Innovate like a fool

Innovate for innovation’s sake. This isn’t a medical procedure, it’s a rule system for a game. If we see a way to ditch experience then let’s do it. Sure, we could divide a point into 100 experience and it would let us balance the game better, but let’s not. Let’s not do levels either, carrying around points has been done less. And how about GAME OVER if you run out of money? Let’s do a clock too. A real time of day system will let us build great systems around it, imagine the great names we can give to talents for evening people! Above all – introduce hugely ambitious superstructures. A great failure is ten times better than a small success.

Gosh! Even more concept art for our skill icons

+1  Tabletop is god

We believe in great D&D. Not in high fantasy or cyberpunk but in the potential of the underlying tabletop experience. If the Game Master has a great story and the players are competent writers too… tabletop wipes the floor with any other medium. (Literature and video games included.) The Zola, Gombrowicz and Bulgakov of our time are already playing D&D, possibly around one table. The trouble is – the experience cannot be recorded and relayed to others. Tabletop is written on water.

Therefore we believe in video game adaptations of the tabletop experience. Games have had great success adapting tactical combat oriented D&D into video games. (Baldur’s Gate 2, etc). We want to do the same for heavy duty story oriented D&D.

– Comments

Better Living Through C# Coding, Part 2: Magic Of Reflection

(Part 1 can be found here.)

“Mirror, mirror on the wall, what’s the ugliest code of ’em all?”

The answer might very well be the annoying amount of technical code we need to implement in our components. Luckily, it annoyed me enough to start pondering about a better way. Specifically, about using attributes and reflection to hide all that ugliness inside a base class.

In the magical world of unicorns and rainbows, I want my Observer component to look pretty:

DemoInputObserver2.cs

using UnityEngine;
public class DemoInputObserver2 : BaseComponent {
	[InputKey(KeyCode.Space)]
	protected void OnSpacebar() {
		Debug.Log("Spacebar was pressed");
	}
	[InputKey(KeyCode.Escape, priority = 10001)]
	protected void OnEscape() {
		Debug.Log("Escape was pressed");
	}
	[InputAxis("Horizontal")]
	protected void OnHorizontal(float value) {
		Debug.Log("Horizontal axis = " + value.ToString());
	}
}

 

The idea is to use custom attributes to establish usage context: what type of input do we want  to handle, and values for relevant parameters (KeyCode, name of the axis, handling priority, etc.). We rely on a common base class to discover those attributes and register thus marked methods with input manager.

Implementation-wise, starting with simple things, the custom attributes themselves are quite straight-forward:

InputAttribute.cs

using UnityEngine;
public abstract class InputAttribute : System.Attribute {
	protected int? _priority;
	public bool hasPriority { get { return _priority!=null; } }
	public int priority { 
		get { return _priority!=null ? (int)_priority : 0; } 
		set { _priority = value; } 
	}
	public InputAttribute() { _priority = null; }
}
/// <summary>Attaching this attribute to a method marks it as HotKeyEventData handler. That method should have a single HotKeyEventData type argument.</summary>
[System.AttributeUsage(System.AttributeTargets.Method)]
public class InputEventDataAttribute : InputAttribute {
	public InputEventDataAttribute() : base() {}
}
/// <summary>Attaching this attribute to a method marks it as Input.GetAxis value handler. That method should have a single float type argument.</summary>
[System.AttributeUsage(System.AttributeTargets.Method)]
public class InputAxisAttribute : InputAttribute {
	public string axis { get; set; }
	public bool IsAxis(string a) { return !string.IsNullOrEmpty(a) && string.Equals(axis, a); }
	public InputAxisAttribute(string axis) : base() { this.axis = axis; }
}
/// <summary>Attaching this attribute to a method marks it as Input.GetButtonDown event handler. That method should have no arguments.</summary>
[System.AttributeUsage(System.AttributeTargets.Method)]
public class InputButtonAttribute : InputAttribute {
	public string button { get; set; }
	public bool IsButton(string b) { return !string.IsNullOrEmpty(b) && string.Equals(button, b); }
	public InputButtonAttribute(string button) : base() { this.button = button; }
}
/// <summary>Attaching this attribute to a method marks it as Input.GetKeyDown event handler. That method should have no arguments.</summary>
[System.AttributeUsage(System.AttributeTargets.Method)]
public class InputKeyAttribute : InputAttribute {
	public virtual KeyCode keyCode { get; set; }
	public InputKeyAttribute(KeyCode keyCode) : base() { this.keyCode = keyCode; }
}

 

Reflection also turns out to be easy to use. The base class that you inherit from can be as simple as:

using UnityEngine;
using System.Reflection;
public class BaseComponent : MonoBehaviour {
	protected void InitManagerAgents() {
		MemberInfo[] members = this.GetType().GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
		foreach (MemberInfo m in members) {
			System.Attribute[] attribs = System.Attribute.GetCustomAttributes(m, false);
			foreach (System.Attribute a in attribs) {
				//if (a is UpdateAttribute) UpdateManager.Register(this, m, a as UpdateAttribute);
				if ((a is InputAttribute) && (m is MethodInfo)) InputManager.Register(this, m as MethodInfo, a as InputAttribute);
			}
		}
		//if (this is InputManager.IKeyHandler) InputManager.Register(this as InputManager.IKeyHandler);
	}
	protected virtual void Awake() {
		InitManagerAgents();
	}
}

 

Virtually all of the complexity is left for InputManager to handle. All we’re providing is reference to self, MethodInfo (which has Invoke method for callback), and InputAttribute providing context.

Lo and behold, the magic module.

InputManager.cs

[Persistent, SelfSpawning(name="InputManager")]
public class InputManager : SingletonComponent<InputManager> {
	public interface IKeyHandler {
		KeyCode inputKey { get; }
		void OnInputKey();
	}
	public interface IPriority {
		int inputPriority { get; }
	}
	public class EventData {
		public string axis = null;
		public string button = null;
		public KeyCode keyCode = KeyCode.None;
		public bool used = false;
		public float value = 0f;
		public EventData(KeyCode keyCode) { this.keyCode = keyCode; }
		public EventData(string axis, float value) { this.axis = axis; this.value = value; }
		public EventData(string button) { this.button = button; }
	}
	/// <summary>Register an axis as one of interest.</summary>
	public static void ObserveAxis(string axis) {
		if (!string.IsNullOrEmpty(axis) && Singleton) Singleton.observedAxes.Add(axis);
	}
	/// <summary>Register a button as one of interest.</summary>
	public static void ObserveButton(string button) {
		if (!string.IsNullOrEmpty(button) && Singleton) Singleton.observedButtons.Add(button);
	}
	/// <summary>Register a keycode as one of interest.</summary>
	public static void ObserveKeyCode(KeyCode keyCode) {
		if (keyCode!=KeyCode.None && Singleton) Singleton.observedKeycodes.Add(keyCode);
	}
	/// <summary>Register a handler method for hotkey event with one above currently highest priority.</summary>
	/// <param name="Action">Handler method that is called when hotkey event triggers. That method has one HotKeyEventData parameter.</param>
	public static void Register(System.Action<EventData> Action) {
		if (Action!=null && Singleton!=null) Singleton.GetBlock(Singleton.highestPriority + 1).Event += Action;
	}
	/// <summary>Register a handler method for hotkey event with the specified priority.</summary>
	/// <param name="Action">Handler method that is called when hotkey event triggers. That method has one HotKeyEventData parameter.</param>
	/// <param name="priority">Callbacks are made in order of priority (from the highest to the lowest).</param>
	public static void Register(System.Action<EventData> Action, int priority) {
		if (Action!=null && Singleton!=null) Singleton.GetBlock(priority).Event += Action;
	}
	public static void Register(IKeyHandler handler) { 
		if (handler!=null && Singleton!=null) Singleton.AddAgent(new KeyHandlerAgent(handler));
	}
	public static void Register(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputAttribute attribute) {
		if (methodInfo==null || !parent || !Singleton) return;
		if (attribute is InputEventDataAttribute) {
			Singleton.AddAgent(new EventDataAgent(parent, methodInfo, attribute as InputEventDataAttribute));
		} else if (attribute is InputAxisAttribute) {
			Singleton.AddAgent(new AxisAgent(parent, methodInfo, attribute as InputAxisAttribute));
		} else if (attribute is InputButtonAttribute) {
			Singleton.AddAgent(new ButtonAgent(parent, methodInfo, attribute as InputButtonAttribute));
		} else if (attribute is InputKeyAttribute) {
			Singleton.AddAgent(new KeyCodeAgent(parent, methodInfo, attribute as InputKeyAttribute));
		}
	}
	/// <summary>Unregister a callback method from all timer events.</summary>
	public static void Unregister(System.Action<EventData> Action) {
		if (Action!=null && Singleton!=null) foreach (EventBlock b in Singleton.eventBlocks) b.Event -= Action;
	}
	protected abstract class EventHandlerAgent : Agent {
		protected System.Action<EventData> Action;
		public EventHandlerAgent(Behaviour parent) : base(parent) {}
		public override void Dispose() {
			if (Action!=null) Unregister(Action);
			Action = null;
			base.Dispose();
		}
	}
	protected class EventDataAgent : EventHandlerAgent {
		public EventDataAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputEventDataAttribute attribute) : base(parent) {
			if (IsFinished) return;
			Action = (x => { if (!IsFinished && parent.isActiveAndEnabled) methodInfo.Invoke(parent, new object[] { x }); });
			if (attribute!=null && attribute.hasPriority) Register(Action, attribute.priority);
			else Register(Action);
		}
	}
	protected class AxisAgent : EventHandlerAgent {
		public AxisAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputAxisAttribute attribute) : base(parent) {
			if (IsFinished) return;
			Action = (x => { 
				if (!IsFinished && !x.used && attribute.IsAxis(x.axis) && parent.isActiveAndEnabled) {
					object res = methodInfo.Invoke(parent, new object[] { x.value }); 
					if (res is bool) x.used = (bool)res;
					else x.used = true;
				}
			});
			ObserveAxis(attribute.axis);
			if (attribute.hasPriority) Register(Action, attribute.priority);
			else Register(Action);
		}
	}
	protected class ButtonAgent : EventHandlerAgent {
		public ButtonAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputButtonAttribute attribute) : base(parent) {
			if (IsFinished) return;
			Action = (x => { 
				if (!IsFinished && !x.used && attribute.IsButton(x.button) && parent.isActiveAndEnabled) {
					object res = methodInfo.Invoke(parent, null); 
					if (res is bool) x.used = (bool)res;
					else x.used = true;
				}
			});
			ObserveButton(attribute.button);
			if (attribute.hasPriority) Register(Action, attribute.priority);
			else Register(Action);
		}
	}
	protected class KeyCodeAgent : EventHandlerAgent {
		public KeyCodeAgent(Behaviour parent, System.Reflection.MethodInfo methodInfo, InputKeyAttribute attribute) : base(parent) {
			if (IsFinished) return;
			Action = (x => { 
				if (!IsFinished && !x.used && x.keyCode!=KeyCode.None && x.keyCode==attribute.keyCode && parent.isActiveAndEnabled) {
					object res = methodInfo.Invoke(parent, null); 
					if (res is bool) x.used = (bool)res;
					else x.used = true;
				}
			});
			ObserveKeyCode(attribute.keyCode);
			if (attribute.hasPriority) Register(Action, attribute.priority);
			else Register(Action);
		}
	}
	protected class KeyHandlerAgent : EventHandlerAgent {
		public KeyHandlerAgent(IKeyHandler handler) : base(handler as Behaviour) {
			if (IsFinished) return;
			Action = (x => { 
				if (!IsFinished && !x.used && x.keyCode!=KeyCode.None && x.keyCode==handler.inputKey && parent.isActiveAndEnabled) {
					handler.OnInputKey();
					x.used = true;
				}
			});
			ObserveKeyCode(handler.inputKey);
			if (parent is IPriority) Register(Action, (parent as IPriority).inputPriority);
			else Register(Action);
		}
	}
	protected class EventBlock : System.IComparable<EventBlock> {
		public int priority;
		public event System.Action<EventData> Event;
		public EventBlock(int p) { priority = p; }
		public void AppendTo(ref System.Action<EventData> deleg) { if (Event!=null) deleg += Event; }
		// Order highest to lowest
		public int CompareTo(EventBlock other) { return -priority.CompareTo(other.priority); }
		public void Invoke(EventData eventData) { if (Event!=null) Event(eventData); }
		public bool IsEmpty { get { return Event==null; } }
	}
	protected AgentCollection agents = null;
	protected List<EventBlock> eventBlocks = new List<EventBlock>();
	protected HashSet<string> observedAxes = new HashSet<string>();
	protected HashSet<string> observedButtons = new HashSet<string>();
	protected HashSet<KeyCode> observedKeycodes = new HashSet<KeyCode>();
	protected bool AddAgent(Agent item) {
		if (agents==null) agents = new AgentCollection();
		return agents.Add(item);
	} 
	protected EventBlock GetBlock(int priority) {
		foreach (EventBlock b in eventBlocks) if (b.priority==priority) return b;
		EventBlock newBlock = new EventBlock(priority);
		eventBlocks.Add(newBlock);
		eventBlocks.Sort();
		return newBlock;
	}
	protected int highestPriority { 
		get {
			// eventBlocks is always sorted in reversed priority order (i.e., highest to lowest), so first non-empty block is the correct result
			foreach (EventBlock b in eventBlocks) if (b.priority<10000 && !b.IsEmpty) return b.priority;
			return 0;
		}
	}
	protected void SendEvent(EventData data) {
		System.Action<EventData> callStack = null;
		foreach (EventBlock block in eventBlocks) block.AppendTo(ref callStack);
		if (callStack!=null) callStack(data);
	}
	protected void Update() {
		foreach (KeyCode k in observedKeycodes) {
			if (Input.GetKeyDown(k)) SendEvent(new EventData(k));
		}
		foreach (string a in observedAxes) {
			SendEvent(new EventData(a, Input.GetAxis(a)));
		}
		foreach (string b in observedButtons) {
			if (Input.GetButtonDown(b)) SendEvent(new EventData(b));
		}
		agents.Update(Time.deltaTime);
	}
	protected override void OnDestroy() { if (agents!=null) agents.Clear(); base.OnDestroy(); }
}

 

As you can see, there’s a bit more going on here than I have revealed the code for. All that has to wait for Part 3, as time is up for today. Sorry!

 

Comments

On skill checks

In the 15 year build up to No Truce With The Furies, one of our main gripes with RPG-s has always been non-combat skill use. In RPGs – even the story-heavy ones – combat is lavished with tactical tension, skill use produces cool chunky animations, you get pulse-raising rewards and punishments, the logos are colourful. Sound effects go “Tring-trang!” and “Pow!”, there are intricate beautiful systems for you to delve into.

Most of this is missing from non-combat skill use. Talking and exploring gets a simplified, non-competitive version of the combat rules. Usually this comes in the form of passive dialogue options: have this much of that required skill and you’ll be able to say this thing. Even the games we truly admire – Planescape: Torment, Mask of the Betrayer, Fallout – have little going on in the rules department when it comes to dialogue. Ditto for most tabletop pen-and-paper role playing systems. The tactical depth of using arguments, employing logic, original thinking, empathy – the skill use that covers 95% of our actual lives – makes up 5% of the rule system. Yet my experience tells me thinking is the ultimate game. It’s nerve-wrecking, conversations are filled with hidden doubts; we struggle to trust each other, manipulate each other, stay sane. There is great strategic depth and tactical tension that goes into talking that games haven’t really – for me – begun to represent yet.

So that’s the first thing we set out to create: a truly in depth non-combat skill system. We have four stats and under each stat there are 6 skills. That gives us 24 skills – all 24 have critical non-combat use. In fact, No Truce With The Furies (the first implementation of our role playing system) will cover their non-combat use almost exclusively. (In the future we want every skill to be a two-faced Janus with somewhat unsymmetrical and unexpected uses in combat and outside it).

I’ll show off the individual skills in a future post. But first I want to talk about how the skills are used in No Truce With The Furies. That is – about skill checks.

Reference used to create the look of our dialogue system

In role-playing games the check is the moment the system “checks” if a character has enough points in a skill to perform an action. It’s a “you have to be at least this tall to ride the rollercoaster” kind of deal. Of course there are exceptions and interesting ideas around, but this is how RPGs usually handle skill checks: your character is talking to someone, that someone lies, if your character has 5 INTELLIGENCE you get a dialogue option that says: “You’re not telling me the truth”. Saying that will make the guy admit he lied. This type of check is called passive because you’re doing nothing. Some hours ago you put two points in “seeing through lies skill” and now the software affirms your choice. There’s not a lot of game in there. And certainly not a lot of literature.

When designing our skill checks in dialogues we had two goals:

  1. Make dialogue more like literature – rethink passive checks
  2. Make dialogue more like a game – add active checks

PASSIVE CHECKS

In literature dialogues are interspersed with thoughts, emotions, alterior motives and physical phenomenon taking place within the characters while they talk. This comes in the form of parenthesis, streams of consiousness, author interjections etc. A whole plethora of literary devices. We wanted to do that in game form. To depict what’s below the surface: the moment an idea forms, the sense of self delusion, secretly laughing because you came up with a stupid joke. Then trying to figure out if you should say it out or not…

It was surprisingly easy to achieve – your skills talk to you. When we use passive checks they are not dialogue options but extra “characters” who silently interject. Only you, the main character can hear them because they are your thoughts, your sensations. Our passive checks are souffleurs in a play.

Let’s look at a sample situation from the game. And remember: every time the main character speaks they have options to say something else. (I have simplified the choice part of the dialogue for the sake of this example).

You come upon a loitering teenage girl kneeling on the ice with a tape recorder in hand. You approach her, question her, then this happens:

You:  “What’s that device you have there?”
Acele:  “This? It’s a portable recording device. It’s for field recording. Low quality, but still.”
You:  “And the wires?”
Acele:  “Actually just one wire, I picked on it ’til the braiding came loose. The wire leads to a contact microphone.”
You:  “What is a “contact microphone”?”
Acele:  “A contact mic is a microphone that records sounds from inside things. Like this ice.”
TRIVIA (difficult success):  Your mangled brain would like you to know there is a boxer called Contact Mike.
You:  What am I supposed to do with this?
TRIVIA:  No idea.
You:  “Does this have anything to do with Contact Mike?”
Acele:  “Uh…” She’s confused. “Yeah, I record stuff with it.”
You:  “No, I mean the boxer Contact Mike.”
Acele:  “Ah! No. This is a *contact microphone*, it’s for recording *inside* solid objects. Contact Mike just beats people up.”
You:  “You know, Contact Mike doesn’t “just beat people up”. Contact Mike is a role model.”
Acele:  “Um…”
You:  “On second thought, screw Contact Mike. He’s no true champion – you are! Look at you here in front of a saggy tent, picking your nose to drug-addict music. The world of sports is in awe of your faith and dedication!”
Acele:  “Man, you are one weird cop.”
You:  “This isn’t about me. This is about your lack of respect for one of boxing’s greats – and for *yourself*.”

This dialogue could have gone differently if you didn’t have a ridiculously detailed (and mostly useless) factual memory. Even then you could have ignored the little connection your mind made, but in this situation the player chose to go off on a tangeant.

What happened was

  1. First you had a high enough Trivia skill.
  2. Then your Trivia told you an “interesting” fact.
  3. Then you had a little conversation with that part of your memory.
  4. Then you reached a hub of questions to Acele where in addition to normal, situation-appropriate ones you had “Does this have anything to do with Contact Mike?”.

This line we call a black check. It’s a line of dialogue fed to you by a passive check. It’s the closest we have to a “have this much skill to get dialogue option” type of affair, but 1) it’s covert, often you don’t even understand where an idea came from 2) we always have the conception of an idea first: the skill talks to you and then sometimes you can use this idea on whoever you’re talking to. If you choose to. Keeping the tidbit to yourself produces effects down the line too, since we consider all dialogue options seen by the player to be ideas circulating in the character’s psyche. Some just remain unspoken.

On some occasions the passive check just makes little observations that lead to more things later, but remain one-liners for now.

So this is how we’ve re-thought passive checks. The versatility of this simple system – let me just repeat it one more time: YOUR SKILLS TALK TO YOU – is pretty incredible. It is hard for us to imagine writing the game without it already. We can do really weird stuff. Like Half Light – the skill that controls your adrenaline gland and your prey drive – can railroad you into a rage spiral where you hound an innocent suspect on something they clearly didn’t do. And it takes another skill’s intervention for you to get out of it. The next moment a skill can wildly expand the options you have avalable, for example: Drama whispers insane method acting ideas into your ear. Or your Pain Threshold tells you to stab yourself in the hand to make a point. Whatever you do – don’t. Pain Threshold is an unstable masochist. It will only leave you screaming with your hand nailed to the table. And then – while screaming with your hand nailed to the table – Rhetoric to the rescue! Make a political point out of this. Tell them you’re a victim of your own macho mentality. Tell them (with your hand still nailed to the table) that years of chauvinism have led you to this low point in your life.

Now, I just made this situation up because I didn’t want to spoil any more of the game, but you get the point. If “Years of chauvinism have led me to this point!” was just a dialogue option it would come out of the blue. But it’s different to hear the thought form in your head out of great physical discomfort and then be able to converse with it. Should I say that? Do I really mean that? You sometimes let these ideas out, sometimes you carry on. We have a game where you might have to start censoring yourself.

contact-mike-gif7

Current version of our dialogue engine. Notice the sexy yet subtle animation cues!

Next time I will talk about active skill checks – our gamey, number crunching, min-maxing counterbalance to the literature-wonk of passive checks.

Til then!