Saturday, October 1, 2016

Pebble Game: 2 Steps RPG Source Code

UPDATE: Complete source is now available at:
http://kamoly.space/projects/pebble/rpg/

The complete source code for my "2 Steps RPG" game is below. It's fairly commented, but it could always use more if I had the time. The code was written specifically for the Pebble watch. If it had been done on a different platform then I'd definitely have taken other approaches, but the following worked well with the performance and memory limitations of the little watch.

Someone could easily treat my code as a simple RPG game engine. Just replace my NPCs with your own, replace my locations with your own, and a maybe few other tweaks here and there and you could have a fairly distinct game ready to play.

If you make a game using my source code, then it'd be nice if you could give me some credit and a link back to my blog on wherever you publish it. Also please let me know about it! I'd love to hear that others found the code useful.

One exception: If you make a game that will likely put you on a government watch list, then please don't give me any kind of credit.  ;-)


Source Code


#include <pebble.h>

#define STORAGE_KEY_SAVE_VERSION 100
#define STORAGE_KEY_PLAYER 101
#define STORAGE_KEY_ENEMY 102

const int currentSaveVersion = 1;

// Storage Keys to record the game state on the Pebble watch when you leave the game.
const uint32_t storageKeySaveVersion = 1; // Save version information for future backwards compatibility.
const uint32_t storageKeyGameSettings = 2; // Current game state (excluding player and enemy).
const uint32_t storageKeyPlayer = 3; // Player's current state.
const uint32_t storageKeyNpc = 4; // NPC's current state.

// Enumerations for the NPC types.
// Locations can either select a specific NPC type or randomly pick from a range.
// In general they should be ordered from lowest threat to highest threat, but it's
// fine having special purposes NPC types in between (as long as you don't want a
// location to pick a random NPC from before AND after the special purpose NPC).
enum NpcTypes
{
  npcTypeNothing,
  npcTypeKing,
  npcTypeInnkeeper,
  npcTypeAttackTrainer,
  npcTypeDefenseTrainer,
  npcTypeCastleGuards,
  npcTypeRabbit,
  npcTypeGopher,
  npcTypeSnake,
  npcTypeGiantAnt,
  npcTypeSquirrel,
  npcTypeSlime,
  npcTypeDeer,
  npcTypeWolf,
  npcTypeOpossum,
  npcTypeWitch,
  npcTypeRaccoon,
  npcTypeRodentOfUnusualSize,
  npcTypeGoblin,
  npcTypeBlackBear,
  npcTypeMoose,
  npcTypeBeaver,
  npcTypeFish,
  npcTypeKobold,
  npcTypeMountainLion,
  npcTypeMountainGoat,
  npcTypeWyvern,
  npcTypeGrizzlyBear,
  npcTypeBighornSheep,
  npcTypeCoyote,
  npcTypeBobcat,
  npcTypeRattlesnake,
  npcTypeBandit,
  npcTypeRabidDog,
  npcTypeSkeleton,
  npcTypeZombie,
  npcTypeGhoul,
  npcTypeOrc,
  npcTypeTroll,
  npcTypeMage,
  npcTypeDragon,
  npcTypeHellBeast,
  npcTypeDemon,
  npcTypeFallenAngel,
  npcTypeGatekeeper,
  npcTypeDeadGatekeeper
};

// Enumerations for travel locations.
// These should be in order from initial location to last location.
// As currently coded, index 0 (locationGreatHall) will be the starting location of the
// game, but the user won't be able to return to it. It will prevent him or her from
// traveling before index 1 (locationInn).
enum TravelLocations
{
  locationGreatHall,
  locationInn,
  locationCombatArena,
  locationDefenseArena,
  locationCastleEntrance,
  locationFieldsOutsideCastle,
  locationRollingFields,
  locationFieldsOutsideForest,
  locationEdgeOfForest,
  locationLightlyWoodedForest,
  locationForest,
  locationHeavilyWoodedForest,
  locationClearingInForest,
  locationNearingAStream,
  locationStreamInForest,
  locationStreamJoiningRiver,
  locationRiverInForest,
  locationAlongRiver,
  locationGentleHill,
  locationSteepRockyHill,
  locationBottomOfMountain,
  locationAlongMountainCliff,
  locationSteepCliffFace,
  locationHighOnRockyCliff,
  locationNearTopOfMountain,
  locationRidgelineOfMountain,
  locationAlongMountainRidge,
  locationRockyMountainside,
  locationLowerMountainFace,
  locationPatchesOfDryGrass,
  locationEdgeOfDesertValley,
  locationDesertValley,
  locationRoadThroughDesert,
  locationAbandonedCarriages,
  locationDesertOutsideTown,
  locationEntranceToSmallTown,
  locationQuietDesertedTown,
  locationDilapidatedBuildings,
  locationDestroyedHomes,
  locationMassGrave,
  locationEdgeOfDesertTown,
  locationOpenBarenDesert,
  locationNearingVolcano,
  locationBaseOfVolanco,
  locationLowVolcanoSlope,
  locationSteepVolcanoSlope,
  locationCaveEntrance,
  locationNarrowingCave,
  locationDeepWithinCave,
  locationLavaTubeHole,
  locationWithinLavaTube,
  locationOpeningInLavaTube,
  locationOpenChamber,
  locationNearingGlowingGate,
  locationGatewayFromHell,
  lastLocationIndex = locationGatewayFromHell
};

// Enumerations for player actions, which will be passed between functions
// depending on button presses.
enum PlayerActions
{
  actionNothing,
  actionForward,
  actionBack,
  actionIgnore,
  actionFight,
  actionFlee,
  actionCastHeal,
  actionRest, // At Inn, get Hit Points and Spell Points
  actionTrainAttack, // At Trainer
  actionTrainDefense, // At Trainer
  actionResetGame // At Gate
};

// Constant to control how quickly player will raise in level.
// Increasing this number will mean it takes longer to reach the next level.
const int nextLevelExperienceIncrement = 15;

// Constant for the cost of resting at the inn.
const int costRest = 1;

// Constant for the cost of training. Cost is the multiplier * the player's current level.
const int costTrainMultiplier = 4;

// Constant for the cost in spell points to cast heal.
const int spellPointsHeal = 1;

// Structure saved in the Pebble when the app is closed and read when the app is open
// that contains various values so the game can resume right where the player left off.
struct gameSettingsStructure
{
  int lowestMovesToWin;
  bool gatekeeperKilled;
  int chanceToFleePercent;
  int chanceOfEncounterPercent;
  char locationDescription[64];
};

// Structure of all properties of the character - both player and NPC. This structure
// will be saved in the Pebble when the app is closed and read when the app is open
// so the game can resume right where the player left off.
struct characterStructure
{
  char description[32];
  int type;
  int location;
  int hitPoints;
  int hitPointsMaximum;
  int spellPoints;
  int spellPointsMaximum;
  int attack;
  int defense;
  int gold;
  int level;
  int experience;
  int experienceNextLevel;
  int experienceLastLevel;
  bool targetable;
  bool ignorable;
  int moves;
};

// Window Objects - these are the various screens that may be shown to the player.
static Window *windowGame; // Main Game Screen
static Window *windowConversation; // Conversation Screen

// Layers for the Main Game Screen
static Layer *layerGameCanvas;
static TextLayer *layerGameTextLocation;
static TextLayer *layerGameTextLocationDescription;
static TextLayer *layerGameTextNpcDescription;
static TextLayer *layerGameTextNpcHitPoints;
static TextLayer *layerGameTextPlayerAttack;
static TextLayer *layerGameTextPlayerDefense;
static TextLayer *layerGameTextPlayerExperience;
static TextLayer *layerGameTextPlayerHitPoints;
static TextLayer *layerGameTextPlayerSpellPoints;
static TextLayer *layerGameTextPlayerGold;
static TextLayer *layerGameTextMessage;
static TextLayer *layerGameTextActionUp;
static TextLayer *layerGameTextActionSelect;
static TextLayer *layerGameTextActionDown;

// Layers for the Conversation Screen
static TextLayer *layerConversationTextSpeaker;
static TextLayer *layerConversationTextMessage;
static TextLayer *layerConversationTextActionBack;

// Last Message is shown at the bottom for the player to get feedback that his or
// her action was processed and/or the amount of damage that was dealt.
static char lastMessage[64];

// Variables containing the current game state.
static struct gameSettingsStructure gameSettings;
static struct characterStructure player;
static struct characterStructure npc;

// Function to return the about of experience necessary for a given level.
static int getNextLevelExperience(int level)
{
  // For nextLevelExperienceIncrement = 15:
  // Level 1: 15
  // Level 2: 45
  // Level 3: 90
  // Level 4: 135
  int requiredExperience = 0;
  for (int loopLevel = 1; loopLevel <= level; loopLevel++)
  {
    requiredExperience += nextLevelExperienceIncrement * loopLevel;
  }
  return requiredExperience;
}

// Function to save the entire game state to the Pebble. Called on exit.
static void saveGame()
{
  persist_write_int(storageKeySaveVersion, currentSaveVersion);
  persist_write_data(storageKeyGameSettings, &gameSettings, sizeof(struct gameSettingsStructure));
  persist_write_data(storageKeyPlayer, &player, sizeof(struct characterStructure));
  persist_write_data(storageKeyNpc, &npc, sizeof(struct characterStructure));
  return;
}

// Function to load the entire game state from the Pebble. Called on load.
static bool loadGame()
{
  if (persist_exists(storageKeySaveVersion) && persist_exists(storageKeyGameSettings) && persist_exists(storageKeyPlayer) && persist_exists(storageKeyNpc))
  {
    int storedVersion = persist_read_int(storageKeySaveVersion);
    if (storedVersion == 0)
    {
      // Always force a new game (handy for debugging).
      return false;
    }
    else if (storedVersion == currentSaveVersion)
    {
      persist_read_data(storageKeyGameSettings, &gameSettings, sizeof(struct gameSettingsStructure));
      persist_read_data(storageKeyPlayer, &player, sizeof(struct characterStructure));
      persist_read_data(storageKeyNpc, &npc, sizeof(struct characterStructure));
      return true;
    }
    // In the future if the save variables change, then else conditions can be added for backwards compatibility.
  }
  return false;
}

// Once a new npc.type has been assigned, calling updateNpcProperties populates the
// npc structure will all the initial values.
static void updateNpcProperties()
{
  // chanceToFleePercent starts at 25%, but it increases each time the player attempts to flee so
  // a player isn't absolutely stuck at a difficult guy until he or she is killed.
  gameSettings.chanceToFleePercent = 25;
  switch(npc.type)
  {
    case npcTypeNothing:
      strcpy(npc.description, "");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeKing:
      strcpy(npc.description, "King");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeInnkeeper:
      strcpy(npc.description, "Innkeeper");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeAttackTrainer:
      strcpy(npc.description, "Attack Trainer");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeDefenseTrainer:
      strcpy(npc.description, "Defense Trainer");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeCastleGuards:
      strcpy(npc.description, "Castle Guards");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeRabbit:
      strcpy(npc.description, "Rabbit");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 3;
      npc.hitPointsMaximum = 3;
      npc.attack = 1;
      npc.defense = 2;
      npc.level = 0;
      npc.experience = 1;
      npc.gold = 1;
      break;
    case npcTypeGopher:
      strcpy(npc.description, "Gopher");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 3;
      npc.hitPointsMaximum = 3;
      npc.attack = 2;
      npc.defense = 1;
      npc.level = 0;
      npc.experience = 1;
      npc.gold = 1;
      break;
    case npcTypeSnake:
      strcpy(npc.description, "Snake");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 2;
      npc.hitPointsMaximum = 2;
      npc.attack = 3;
      npc.defense = 3;
      npc.level = 1;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeGiantAnt:
      strcpy(npc.description, "Giant Ant");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 6;
      npc.hitPointsMaximum = 6;
      npc.attack = 4;
      npc.defense = 3;
      npc.level = 2;
      npc.experience = 3;
      npc.gold = 0;
      break;
    case npcTypeSquirrel:
      strcpy(npc.description, "Squirrel");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 2;
      npc.hitPointsMaximum = 2;
      npc.attack = 2;
      npc.defense = 3;
      npc.level = 1;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeSlime:
      strcpy(npc.description, "Slime");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 6;
      npc.defense = 1;
      npc.level = 2;
      npc.experience = 7;
      npc.gold = 0;
      break;
    case npcTypeDeer:
      strcpy(npc.description, "Deer");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 3;
      npc.defense = 6;
      npc.level = 2;
      npc.experience = 8;
      npc.gold = 5;
      break;
    case npcTypeWolf:
      strcpy(npc.description, "Wolf");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 3;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeOpossum:
      strcpy(npc.description, "Opossum");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 6;
      npc.defense = 3;
      npc.level = 3;
      npc.experience = 10;
      npc.gold = 2;
      break;
    case npcTypeWitch:
      strcpy(npc.description, "Witch");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 12;
      npc.defense = 2;
      npc.level = 4;
      npc.experience = 12;
      npc.gold = 20;
      break;
    case npcTypeRaccoon:
      strcpy(npc.description, "Raccoon");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 3;
      npc.defense = 4;
      npc.level = 3;
      npc.experience = 8;
      npc.gold = 3;
      break;
    case npcTypeRodentOfUnusualSize:
      strcpy(npc.description, "Unusual Size Rodent");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 8;
      npc.defense = 3;
      npc.level = 5;
      npc.experience = 11;
      npc.gold = 5;
      break;
    case npcTypeGoblin:
      strcpy(npc.description, "Goblin");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 15;
      npc.hitPointsMaximum = 15;
      npc.attack = 9;
      npc.defense = 5;
      npc.level = 6;
      npc.experience = 11;
      npc.gold = 7;
      break;
    case npcTypeBlackBear:
      strcpy(npc.description, "Black Bear");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 20;
      npc.hitPointsMaximum = 20;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 5;
      npc.experience = 15;
      npc.gold = 5;
      break;
    case npcTypeKobold:
      strcpy(npc.description, "Kobold");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 10;
      npc.defense = 10;
      npc.level = 7;
      npc.experience = 20;
      npc.gold = 14;
      break;
    case npcTypeMoose:
      strcpy(npc.description, "Moose");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 20;
      npc.hitPointsMaximum = 20;
      npc.attack = 8;
      npc.defense = 4;
      npc.level = 6;
      npc.experience = 20;
      npc.gold = 8;
      break;
    case npcTypeBeaver:
      strcpy(npc.description, "Beaver");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 4;
      npc.defense = 2;
      npc.level = 2;
      npc.experience = 7;
      npc.gold = 3;
      break;
    case npcTypeFish:
      strcpy(npc.description, "Fish");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 1;
      npc.hitPointsMaximum = 1;
      npc.attack = 0;
      npc.defense = 2;
      npc.level = 0;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeMountainLion:
      strcpy(npc.description, "Mountain Lion");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 16;
      npc.hitPointsMaximum = 16;
      npc.attack = 8;
      npc.defense = 6;
      npc.level = 7;
      npc.experience = 20;
      npc.gold = 10;
      break;
    case npcTypeMountainGoat:
      strcpy(npc.description, "Mountain Goat");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 13;
      npc.hitPointsMaximum = 13;
      npc.attack = 5;
      npc.defense = 5;
      npc.level = 6;
      npc.experience = 17;
      npc.gold = 7;
      break;
    case npcTypeWyvern:
      strcpy(npc.description, "Wyvern");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 15;
      npc.defense = 9;
      npc.level = 9;
      npc.experience = 22;
      npc.gold = 7;
      break;
    case npcTypeGrizzlyBear:
      strcpy(npc.description, "Grizzly Bear");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 28;
      npc.hitPointsMaximum = 28;
      npc.attack = 9;
      npc.defense = 6;
      npc.level = 8;
      npc.experience = 20;
      npc.gold = 10;
      break;
    case npcTypeBighornSheep:
      strcpy(npc.description, "Bighorn Sheep");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 13;
      npc.hitPointsMaximum = 13;
      npc.attack = 5;
      npc.defense = 5;
      npc.level = 5;
      npc.experience = 17;
      npc.gold = 7;
      break;
    case npcTypeCoyote:
      strcpy(npc.description, "Coyote");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 6;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeBobcat:
      strcpy(npc.description, "Bobcat");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 4;
      npc.defense = 6;
      npc.level = 7;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeRattlesnake:
      strcpy(npc.description, "Rattlesnake");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 4;
      npc.hitPointsMaximum = 4;
      npc.attack = 6;
      npc.defense = 3;
      npc.level = 4;
      npc.experience = 7;
      npc.gold = 2;
      break;
    case npcTypeBandit:
      strcpy(npc.description, "Bandit");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 12;
      npc.defense = 12;
      npc.level = 10;
      npc.experience = 30;
      npc.gold = 14;
      break;
    case npcTypeRabidDog:
      strcpy(npc.description, "Rabid Dog");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 10;
      npc.defense = 4;
      npc.level = 9;
      npc.experience = 10;
      npc.gold = 0;
      break;
    case npcTypeSkeleton:
      strcpy(npc.description, "Skeleton");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 15;
      npc.hitPointsMaximum = 15;
      npc.attack = 8;
      npc.defense = 8;
      npc.level = 12;
      npc.experience = 12;
      npc.gold = 0;
      break;
    case npcTypeZombie:
      strcpy(npc.description, "Zombie");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 14;
      npc.defense = 6;
      npc.level = 12;
      npc.experience = 15;
      npc.gold = 10;
      break;
    case npcTypeGhoul:
      strcpy(npc.description, "Ghoul");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 15;
      npc.defense = 8;
      npc.level = 11;
      npc.experience = 15;
      npc.gold = 3;
      break;
    case npcTypeOrc:
      strcpy(npc.description, "Orc");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 35;
      npc.hitPointsMaximum = 35;
      npc.attack = 16;
      npc.defense = 12;
      npc.level = 14;
      npc.experience = 30;
      npc.gold = 12;
      break;
    case npcTypeTroll:
      strcpy(npc.description, "Troll");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 45;
      npc.hitPointsMaximum = 45;
      npc.attack = 18;
      npc.defense = 10;
      npc.level = 15;
      npc.experience = 35;
      npc.gold = 15;
      break;
    case npcTypeMage:
      strcpy(npc.description, "Mage");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 20;
      npc.defense = 10;
      npc.level = 16;
      npc.experience = 40;
      npc.gold = 20;
      break;
    case npcTypeDragon:
      strcpy(npc.description, "Dragon");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 50;
      npc.hitPointsMaximum = 50;
      npc.attack = 22;
      npc.defense = 22;
      npc.level = 19;
      npc.experience = 60;
      npc.gold = 40;
      break;
    case npcTypeHellBeast:
      strcpy(npc.description, "Hell Beast");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 40;
      npc.hitPointsMaximum = 40;
      npc.attack = 18;
      npc.defense = 16;
      npc.level = 17;
      npc.experience = 60;
      npc.gold = 30;
      break;
    case npcTypeDemon:
      strcpy(npc.description, "Demon");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 50;
      npc.hitPointsMaximum = 50;
      npc.attack = 20;
      npc.defense = 18;
      npc.level = 18;
      npc.experience = 60;
      npc.gold = 30;
      break;
    case npcTypeFallenAngel:
      strcpy(npc.description, "Fallen Angel");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 70;
      npc.hitPointsMaximum = 70;
      npc.attack = 25;
      npc.defense = 25;
      npc.level = 20;
      npc.experience = 60;
      npc.gold = 35;
      break;
    case npcTypeGatekeeper:
      strcpy(npc.description, "Gatekeeper");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 100;
      npc.hitPointsMaximum = 100;
      npc.attack = 30;
      npc.defense = 30;
      npc.level = 23;
      npc.experience = 1000;
      npc.gold = 0;
      break;
    case npcTypeDeadGatekeeper:
      strcpy(npc.description, "Dead Gatekeeper");
      npc.targetable = false;
      npc.ignorable = false;
      npc.hitPoints = 1;
      npc.hitPointsMaximum = 1;
      npc.attack = 1;
      npc.defense = 1;
      npc.level = 1;
      npc.experience = 1;
      npc.gold = 0;
      break;
  }
}

// Function that will return a NPC type given a chance of encounter, the lower NPC enumeration,
// and the upper NPC enumeration. This allows locations to have a specific range of NPC types
// possible with a chance of encountering them.
static int getNpcType(int chanceOfNpc, int lowerRange, int upperRange)
{
  gameSettings.chanceOfEncounterPercent += chanceOfNpc;
  if (gameSettings.chanceOfEncounterPercent > rand() % 100)
  {
    gameSettings.chanceOfEncounterPercent = 0;
    return rand() % (upperRange - lowerRange + 1) + lowerRange;
  }
  else
  {
    return 0;
  }
}

// Once a new player.location has been assigned, calling updateLocationProperties populates the
// location structure will all the initial values. This function also assigns a NPC appropriate
// for the new location. Some locations always have the same NPCs, some locations have a random
// range. Some locations may change their properties depending on other game settings.
static void updateLocationProperties()
{
  switch(player.location)
  {
    case locationGreatHall:
      strcpy(gameSettings.locationDescription, "Castle Great Hall");
      npc.type = npcTypeKing;
      break;
    case locationInn:
      strcpy(gameSettings.locationDescription, "Inn, $1");
      npc.type = npcTypeInnkeeper;
      break;
    case locationCombatArena:
      snprintf(gameSettings.locationDescription, sizeof(gameSettings.locationDescription), "Combat Arena  $%d*Att", costTrainMultiplier);
      npc.type = npcTypeAttackTrainer;
      break;
    case locationDefenseArena:
      snprintf(gameSettings.locationDescription, sizeof(gameSettings.locationDescription), "Defense Arena  $%d*Def", costTrainMultiplier);
      npc.type = npcTypeDefenseTrainer;
      break;
    case locationCastleEntrance:
      strcpy(gameSettings.locationDescription, "Castle Entrance");
      npc.type = npcTypeCastleGuards;
      break;
    case locationFieldsOutsideCastle:
      strcpy(gameSettings.locationDescription, "Fields Outside Castle");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationRollingFields:
      strcpy(gameSettings.locationDescription, "Rolling Fields");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationFieldsOutsideForest:
      strcpy(gameSettings.locationDescription, "Fields Outside Forest");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationEdgeOfForest:
      strcpy(gameSettings.locationDescription, "Edge of Forest");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeSnake);
      break;
    case locationLightlyWoodedForest:
      strcpy(gameSettings.locationDescription, "Lightly Wooded Forest");
      npc.type = getNpcType(25, npcTypeSnake, npcTypeSquirrel);
      break;
    case locationForest:
      strcpy(gameSettings.locationDescription, "Forest");
      npc.type = getNpcType(25, npcTypeGiantAnt, npcTypeDeer);
      break;
    case locationHeavilyWoodedForest:
      strcpy(gameSettings.locationDescription, "Heavily Wooded Forest");
      npc.type = getNpcType(25, npcTypeGiantAnt, npcTypeWolf);
      break;
    case locationClearingInForest:
      strcpy(gameSettings.locationDescription, "Clearing in Forest");
      npc.type = getNpcType(25, npcTypeDeer, npcTypeMoose);
      break;
    case locationNearingAStream:
      strcpy(gameSettings.locationDescription, "Nearing a Stream");
      npc.type = getNpcType(25, npcTypeDeer, npcTypeMoose);
      break;
    case locationStreamInForest:
      strcpy(gameSettings.locationDescription, "Stream in Forest");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationStreamJoiningRiver:
      strcpy(gameSettings.locationDescription, "Stream Joining River");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationRiverInForest:
      strcpy(gameSettings.locationDescription, "River in Forest");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationAlongRiver:
      strcpy(gameSettings.locationDescription, "Along River, Few Trees");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationGentleHill:
      strcpy(gameSettings.locationDescription, "Gentle Hill");
      npc.type = getNpcType(25, npcTypeKobold, npcTypeGrizzlyBear);
      break;
    case locationSteepRockyHill:
      strcpy(gameSettings.locationDescription, "Steep Rocky Hill");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationBottomOfMountain:
      strcpy(gameSettings.locationDescription, "Bottom of Mountain");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationAlongMountainCliff:
      strcpy(gameSettings.locationDescription, "Along Mountain Cliff");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationSteepCliffFace:
      strcpy(gameSettings.locationDescription, "Steep Cliff Face");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationHighOnRockyCliff:
      strcpy(gameSettings.locationDescription, "High on Rocky Cliff");
      npc.type = getNpcType(25, npcTypeGrizzlyBear, npcTypeBobcat);
      break;
    case locationNearTopOfMountain:
      strcpy(gameSettings.locationDescription, "Near Top of Mountain");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationRidgelineOfMountain:
      strcpy(gameSettings.locationDescription, "Ridgeline of Mountain");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationAlongMountainRidge:
      strcpy(gameSettings.locationDescription, "Along Mountain Ridge");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationRockyMountainside:
      strcpy(gameSettings.locationDescription, "Rocky Mountainside");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRattlesnake);
      break;
    case locationLowerMountainFace:
      strcpy(gameSettings.locationDescription, "Lower Mountain Face");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRattlesnake);
      break;
    case locationPatchesOfDryGrass:
      strcpy(gameSettings.locationDescription, "Patches of Dry Grass");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBandit);
      break;
    case locationEdgeOfDesertValley:
      strcpy(gameSettings.locationDescription, "Edge of Desert Valley");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBandit);
      break;
    case locationDesertValley:
      strcpy(gameSettings.locationDescription, "Desert Valley");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRabidDog);
      break;
    case locationRoadThroughDesert:
      strcpy(gameSettings.locationDescription, "Road through Desert");
      npc.type = getNpcType(25, npcTypeRattlesnake, npcTypeSkeleton);
      break;
    case locationAbandonedCarriages:
      strcpy(gameSettings.locationDescription, "Abandoned Carriages");
      npc.type = getNpcType(25, npcTypeRattlesnake, npcTypeSkeleton);
      break;
    case locationDesertOutsideTown:
      strcpy(gameSettings.locationDescription, "Desert Outside a Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationEntranceToSmallTown:
      strcpy(gameSettings.locationDescription, "Entrance to Small Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationQuietDesertedTown:
      strcpy(gameSettings.locationDescription, "Quiet Deserted Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationDilapidatedBuildings:
      strcpy(gameSettings.locationDescription, "Dilapidated Buildings");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationDestroyedHomes:
      strcpy(gameSettings.locationDescription, "Destroyed Homes");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationMassGrave:
      strcpy(gameSettings.locationDescription, "Mass Grave");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationEdgeOfDesertTown:
      strcpy(gameSettings.locationDescription, "Edge of Desert Town");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationOpenBarenDesert:
      strcpy(gameSettings.locationDescription, "Open Baren Desert");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeMage);
      break;
    case locationNearingVolcano:
      strcpy(gameSettings.locationDescription, "Nearing Volcano");
      npc.type = getNpcType(25, npcTypeZombie, npcTypeMage);
      break;
    case locationBaseOfVolanco:
      strcpy(gameSettings.locationDescription, "Base of Volcano");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationLowVolcanoSlope:
      strcpy(gameSettings.locationDescription, "Low Volcano Slope");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationSteepVolcanoSlope:
      strcpy(gameSettings.locationDescription, "Steep Volcano Slope");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationCaveEntrance:
      strcpy(gameSettings.locationDescription, "Cave Entrance");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationNarrowingCave:
      strcpy(gameSettings.locationDescription, "Narrowing Cave");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationDeepWithinCave:
      strcpy(gameSettings.locationDescription, "Deep Within Cave");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationLavaTubeHole:
      strcpy(gameSettings.locationDescription, "Lava Tube Hole");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationWithinLavaTube:
      strcpy(gameSettings.locationDescription, "Within Lava Tube");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationOpeningInLavaTube:
      strcpy(gameSettings.locationDescription, "Opening in Lava Tube");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationOpenChamber:
      strcpy(gameSettings.locationDescription, "Open Chamber");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationNearingGlowingGate:
      strcpy(gameSettings.locationDescription, "Nearing Glowing Gate");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationGatewayFromHell:
      if (!gameSettings.gatekeeperKilled)
      {
        strcpy(gameSettings.locationDescription, "Glowing Gate");
        npc.type = npcTypeGatekeeper;      
      }
      else
      {
        strcpy(gameSettings.locationDescription, "Destroyed Gate");
        npc.type = npcTypeDeadGatekeeper;
      }
      break;
    default:
      strcpy(gameSettings.locationDescription, "The Void");
      npc.type = npcTypeNothing;
      break;
  }
  updateNpcProperties();

  return;
}

// Function to reset the entire game to a default state. (Called on new game.)
static void resetGame()
{
  strcpy(player.description, "");
  player.type = 0;
  // Move the player to the very first room (which will be inaccessible later).
  player.location = 0;
  // Assign initial values for the player.
  player.hitPoints = 10;
  player.hitPointsMaximum = 10;
  player.spellPoints = 4;
  player.spellPointsMaximum = 4;
  player.gold = 5;
  player.attack = 1;
  player.defense = 1;
  player.level = 1;
  player.experience = 0;
  player.experienceLastLevel = 0;
  player.experienceNextLevel = getNextLevelExperience(player.level);
  player.targetable = true;
  player.ignorable = false;
  player.moves = 0;

  gameSettings.gatekeeperKilled = false;
  gameSettings.chanceToFleePercent = 25;
  gameSettings.chanceOfEncounterPercent = 0;

  updateLocationProperties();
  return;
}

// Get the action that should be processed when the "Up" button is pressed on the Pebble.
static int getActionUp()
{
  // Up always attempts to bring us further away from the first location.
  // If we're in the middle of combat, then it attempts an attack.
  if (!npc.targetable || npc.type == npcTypeNothing)
  {
    if (player.location == lastLocationIndex)
    {
      return actionNothing;
    }
    else
    {
      return actionForward;
    }
  }
  else if (npc.ignorable)
  {
    return actionFight;
  }
  else
  {
    return actionFight;
  }
}

// Get the action that should be processed when the "Select" button is pressed on the Pebble.
static int getActionSelect()
{
  // The select action is primarily for when we're interacting with a NPC, so the action will be based
  // on the NPC type. For enemy NPCs, we'll default to the heal action (if we're injured).
  switch(npc.type)
  {
    case npcTypeInnkeeper:
      if ((player.gold >= costRest) &&
          ((player.hitPoints < player.hitPointsMaximum) || (player.spellPoints < player.spellPointsMaximum)))
      {
        return actionRest;
      }
      break;
    case npcTypeAttackTrainer:
      if (player.gold >= costTrainMultiplier * player.attack)
      {
        return actionTrainAttack;
      }
      break;
    case npcTypeDefenseTrainer:
      if (player.gold >= costTrainMultiplier * player.defense)
      {
        return actionTrainDefense;
      }
      break;
    case npcTypeDeadGatekeeper:
      return actionResetGame;
      break;
    default:
      if ((player.spellPoints >= spellPointsHeal) && (player.hitPoints < player.hitPointsMaximum))
      {
        return actionCastHeal;
      }
      break;
  }
  return actionNothing;
}

// Get the action that should be processed when the "Down" button is pressed on the Pebble.
static int getActionDown()
{
  // Down always attempts to bring us towards the first location.
  // If we're in the middle of combat with someone we can't ignore, then it attempts a flee.
  if (!npc.targetable || npc.type == npcTypeNothing)
  {
    if (player.location > 1)
    {
      return actionBack;
    }
    else
    {
      return actionNothing;
    }
  }
  else if (npc.ignorable)
  {
    return actionIgnore;
  }
  else
  {
    return actionFlee;
  }
}

// Function to get a text description (for display) of a given action.
static void getActionDescription(int action, char *string)
{
  switch(action)
  {
    case actionNothing:
      strcpy(string, "");
      break;
    case actionForward:
      strcpy(string, "Fwd");
      break;
    case actionBack:
      strcpy(string, "Back");
      break;
    case actionIgnore:
      strcpy(string, "Ignore");
      break;
    case actionFight:
      strcpy(string, "Fight");
      break;
    case actionFlee:
      strcpy(string, "Flee");
      break;
    case actionCastHeal:
      strcpy(string, "Cast Heal");
      break;
    case actionRest:
      strcpy(string, "Rest");
      break;
    case actionTrainAttack:
      strcpy(string, "Train");
      break;
    case actionTrainDefense:
      strcpy(string, "Train");
      break;
    case actionResetGame:
      strcpy(string, "New Game");
      break;
  }
  return;
}

// Call to update the Main Game Screen for the player.
static void updateGameWindow()
{
  // Label the Up/Select/Down buttons.
  static char bufferActionUp[64];
  static char bufferActionSelect[64];
  static char bufferActionDown[64];
  getActionDescription(getActionUp(), bufferActionUp);
  getActionDescription(getActionSelect(), bufferActionSelect);
  getActionDescription(getActionDown(), bufferActionDown);
  text_layer_set_text(layerGameTextActionUp, bufferActionUp);
  text_layer_set_text(layerGameTextActionSelect, bufferActionSelect);
  text_layer_set_text(layerGameTextActionDown, bufferActionDown);

  // Show player location number and description.
  static char bufferPlayerLocation[64];
  snprintf(bufferPlayerLocation, sizeof(bufferPlayerLocation), "Location:%d", player.location);
  text_layer_set_text(layerGameTextLocation, bufferPlayerLocation);

  static char bufferPlayerLocationDescription[64];
  strcpy(bufferPlayerLocationDescription, gameSettings.locationDescription);
  text_layer_set_text(layerGameTextLocationDescription, bufferPlayerLocationDescription);

  // If a NPC is present, show its information.
  static char bufferNpcDescription[32];
  if (npc.type > npcTypeNothing)
  {
    strcpy(bufferNpcDescription, npc.description);
  }
  else
  {
    strcpy(bufferNpcDescription, "");
  }
  text_layer_set_text(layerGameTextNpcDescription, bufferNpcDescription);

  static char bufferNpcHitPoints[32];
  if (npc.targetable && npc.type > npcTypeNothing && npc.hitPoints > 0)
  {
    snprintf(bufferNpcHitPoints, sizeof(bufferNpcHitPoints), "HP:%d", npc.hitPoints); //, npc.hitPointsMaximum);
  }
  else
  {
    strcpy(bufferNpcHitPoints, "");  
  }
  text_layer_set_text(layerGameTextNpcHitPoints, bufferNpcHitPoints);

  // Show the player's current properties.

  static char bufferPlayerAttack[64];
  snprintf(bufferPlayerAttack, sizeof(bufferPlayerAttack), "Att:%d", player.attack);
  text_layer_set_text(layerGameTextPlayerAttack, bufferPlayerAttack);

  static char bufferPlayerDefense[64];
  snprintf(bufferPlayerDefense, sizeof(bufferPlayerDefense), "Def:%d", player.defense);
  text_layer_set_text(layerGameTextPlayerDefense, bufferPlayerDefense);

  static char bufferPlayerExperience[64];
  snprintf(bufferPlayerExperience, sizeof(bufferPlayerExperience), "Level:%d", player.level);
  text_layer_set_text(layerGameTextPlayerExperience, bufferPlayerExperience);

  static char bufferPlayerHitPoints[64];
  snprintf(bufferPlayerHitPoints, sizeof(bufferPlayerHitPoints), "HP:%d/%d", player.hitPoints, player.hitPointsMaximum);
  text_layer_set_text(layerGameTextPlayerHitPoints, bufferPlayerHitPoints);

  static char bufferPlayerSpellPoints[64];
  snprintf(bufferPlayerSpellPoints, sizeof(bufferPlayerSpellPoints), "SP:%d/%d", player.spellPoints, player.spellPointsMaximum);
  text_layer_set_text(layerGameTextPlayerSpellPoints, bufferPlayerSpellPoints);

  static char bufferPlayerGold[64];
  snprintf(bufferPlayerGold, sizeof(bufferPlayerGold), "$%d", player.gold);
  text_layer_set_text(layerGameTextPlayerGold, bufferPlayerGold);

  // Show the last message to the player (for feedback from the last action taken).

  static char bufferMessage[64];
  strcpy(bufferMessage, lastMessage);
  text_layer_set_text(layerGameTextMessage, bufferMessage);

  return;
}

// Call to update the Conversation Screen for the player.
static void updateConversationWindow()
{
  static char bufferConversationSpeaker[64];
  static char bufferConversationMessage[256];
  static char bufferConversationTextActionBack[64];

  // Conversations are currently very simple. It's only dependent upon the NPC you are
  // encountering and they each have only one thing to say.
  switch (npc.type)
  {
    case npcTypeKing:
      // New character, tell them what's going on!
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "King");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "There is a growing threat to the lands. A dark evil from the east has been murdering citizens and destroying towns. Find what is causing this and put an end to it.");
      break;
    case npcTypeInnkeeper:
      // We died! Give a pep-talk.
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "Innkeeper");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "You were found unconscious and dying by a fellow adventurer and brought back to the inn. You lost some of your skills, but rest up and continue your quest.");
      break;
    case npcTypeDeadGatekeeper:
      // We won!
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "The Gate");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "You have closed the gateway from hell! The kingdom is safe again. It took you %i moves, your best is %i moves. You may reset your character to try again.", player.moves, gameSettings.lowestMovesToWin);
      break;
    default:
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), " ");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage), " ");
      break;
  }

  // Label for the Pebble back button.
  snprintf(bufferConversationTextActionBack, sizeof(bufferConversationTextActionBack), "Press Back");

  text_layer_set_text(layerConversationTextSpeaker, bufferConversationSpeaker);
  text_layer_set_text(layerConversationTextMessage, bufferConversationMessage);
  text_layer_set_text(layerConversationTextActionBack, bufferConversationTextActionBack);
  return;
}

// Function process the forward action.
static void processActionForward()
{
  if (player.location < lastLocationIndex)
  {
    player.location += 1;
    player.moves += 1;
    updateLocationProperties();
    snprintf(lastMessage, sizeof(lastMessage), "Moved forward.");
    updateGameWindow();
  }
  return;
}

// Function to process the back action.
static void processActionBack()
{
  if (player.location > 1)
  {
    player.location -= 1;
    player.moves += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Moved back.");
    updateLocationProperties();
    updateGameWindow();
  }
  return;
}

// Function to process the ignore action.
static void processActionIgnore()
{
  npc.type = 0;
  snprintf(lastMessage, sizeof(lastMessage), "Ignored enemy.");
  updateGameWindow();
  return;
}

// Function to process an attack given an attacker and defender. The attacker could be the
// player or NPC (and the defender would be the NPC or player respectively).
static int processAttack(struct characterStructure *attacker, struct characterStructure *defender)
{
  // Math to determine whether or not we should have hit.
  int chanceToHitPercent = (15 * (1 + attacker->level - defender->level)) + (2 * (attacker->attack - defender->defense));
  // No matter how underpowered the attacker is, always give a chance to hit.
  if (chanceToHitPercent < 25)
  {
    chanceToHitPercent = 25;
  }

  int damage = 0;
  if (chanceToHitPercent >= (rand() % 100))
  {
    // Basing the damage range as the difference between the attacker and defender resulted in too many 1 HP hits.
    //int damageRange = (attacker->level + attacker->attack) - (defender->level + defender->defense);
    int damageRange = attacker->attack;
    if (damageRange < 1)
    {
      // Even if the defender is significantly more powerful than attacker, attacker should be able do something.
      damageRange = 1;
    }
    damage = rand() % damageRange;
    if (damage < 1)
    {
      damage = 1;
    }
  }

  return damage;
}

// Function called when the player has been killed.
static void processDeath()
{
  snprintf(lastMessage, sizeof(lastMessage), " ");

  // Set the player back to the start and bump the hit and spell points back up.
  player.location = 1;
  player.hitPoints = player.hitPointsMaximum;
  player.spellPoints = player.spellPointsMaximum;

  // We need some punishment for dying, take away his or her cash and reduce skills.
  player.gold = 0;
  player.attack -= 1;
  if (player.attack < 1)
  {
    player.attack = 1;
  }
  player.defense -= 1;
  if (player.defense < 1)
  {
    player.defense = 1;
  }

  // Return back to the inn.
  updateLocationProperties();

  // Show the conversation with the innkeeper.
  updateConversationWindow();
  window_stack_push(windowConversation, true);

  return;
}

// Function called when the player leveled up.
static void processLevelUp()
{
  player.level += 1;
  player.experienceLastLevel = player.experienceNextLevel;
  player.experienceNextLevel = getNextLevelExperience(player.level);
  player.hitPointsMaximum += 3;
  player.spellPointsMaximum += 2;
  // Makes the game too easy if we restore hit/spell points on level up.
  //player.hitPoints = player.hitPointsMaximum;
  //player.spellPoints = player.spellPointsMaximum;
  return;
}

// Function to process the fight action.
static void processActionFight()
{
  if (npc.targetable && npc.type > npcTypeNothing)
  {
    // Attack the enemy.
 
    // If the player and npc have similar ability, then it could take many attacks before a blow is landed.
    // In those cases, users were constantly pushing the fight button and the message window would just say
    // hit 0 lost 0. That's silly though, we might as well reroll until someone hits.
    int deliveredDamage = 0;
    int receivedDamage = 0;
    // Although at some point a while loop should eventually return some damage, let's not risk a race
    // condition and just loop a maximum of 10 times.
    for (int attackLoop = 0; attackLoop < 10; attackLoop++)
    {
      deliveredDamage = processAttack(&player, &npc);
      receivedDamage = processAttack(&npc, &player);
      if ((deliveredDamage > 0) || (receivedDamage > 0))
      {
        // Someone got a hit in, exit the loop.
        break;
      }
    }
 
    // Always deal the damage (if any) to the npc. We'll only take damage ourselves if the npc is still alive.
    npc.hitPoints -= deliveredDamage;
 
    if(npc.hitPoints > 0)
    {
      // Enemy still alive, take the damage is dealt us.
      player.hitPoints -= receivedDamage;
      snprintf(lastMessage, sizeof(lastMessage), "Hit %d, lost %d HP!", deliveredDamage, receivedDamage);
      if (player.hitPoints <= 0)
      {
        // We've been killed!
        processDeath();
      }
    }
    else
    {
      // Killed the enemy.
      int npcExperience = (npc.hitPointsMaximum + npc.attack + npc.defense + npc.level) / 4;
      if (npcExperience < 1)
      {
        npcExperience = 1;
      }
      player.experience += npcExperience;
      int npcGold = npc.gold;
      player.gold += npcGold;

      if (npc.type == npcTypeGatekeeper)
      {
        // We won, show winning screen!
        gameSettings.gatekeeperKilled = true;
        if ((player.moves < gameSettings.lowestMovesToWin) || (gameSettings.lowestMovesToWin == 0))
        {
          gameSettings.lowestMovesToWin = player.moves;
        }
        // Reload the location to get a new room description and dead gatekeeper npc.
        updateLocationProperties();
        updateGameWindow();
        window_stack_push(windowConversation, true);
      }
      else
      {
        // Clear out the room.
        npc.type = npcTypeNothing;
        updateNpcProperties();
      }
   
      if (player.experience > player.experienceNextLevel)
      {
        processLevelUp();
        snprintf(lastMessage, sizeof(lastMessage), "Killed! Leveled!");
      }
      else
      {
        snprintf(lastMessage, sizeof(lastMessage), "Killed! Got $%d", npcGold);
      }
    }
  } // npc.targetable && npc.type > npcTypeNothing
  updateGameWindow();
  return;
}

// Function to process the flee action.
static void processActionFlee()
{
  if (gameSettings.chanceToFleePercent > rand() % 100)
  {
    // Successfully fled!
    processActionBack();
    snprintf(lastMessage, sizeof(lastMessage), "Fled back!");
  }
  else
  {
    // Failed to flee, but make the next attempt more likely.
    gameSettings.chanceToFleePercent += 25;
    // Take damage while we're fleeing.
    int receivedDamage = processAttack(&npc, &player);
    if (receivedDamage > 0)
    {
      player.hitPoints -= receivedDamage;
      snprintf(lastMessage, sizeof(lastMessage), "Blocked, lost %d!", receivedDamage);
      if (player.hitPoints <= 0)
      {
        // We've been killed!
        processDeath();
      }
    }
    else
    {
      snprintf(lastMessage, sizeof(lastMessage), "Blocked!");
    }
  }
  updateGameWindow();
  return;
}

// Function to process the heal action.
static void processActionCastHeal()
{
  if (player.spellPoints >= spellPointsHeal)
  {
    player.spellPoints -= spellPointsHeal;
    player.hitPoints = player.hitPointsMaximum;
    if (npc.targetable && !npc.ignorable && npc.type > npcTypeNothing)
    {
      int receivedDamage = processAttack(&npc, &player);
      if (receivedDamage > 0)
      {
        player.hitPoints -= receivedDamage;
        snprintf(lastMessage, sizeof(lastMessage), "Healed, lost %d!", receivedDamage);
      }
      else
      {
        snprintf(lastMessage, sizeof(lastMessage), "Healed!");
      }
    }
    else
    {
        snprintf(lastMessage, sizeof(lastMessage), "Healed.");
    }
  }
  updateGameWindow();
  return;
}

// Function to process the rest action (done at the Inn).
static void processActionRest()
{
  if (player.gold >= costRest)
  {
    player.gold -=costRest;
    player.hitPoints = player.hitPointsMaximum;
    player.spellPoints = player.spellPointsMaximum;
    snprintf(lastMessage, sizeof(lastMessage), "Rested.");
    updateGameWindow();
  }
  return;
}

// Function to process the train attack action (done at the Attack Trainer).
static void processActionTrainAttack()
{
  int costToTrain = costTrainMultiplier * player.attack;
  if (player.gold >= costToTrain)
  {
    player.gold -= costToTrain;
    player.attack += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Trained attack.");
    updateGameWindow();
  }
  return;
}

// Function to process the train defense action (done at the Defense Trainer).
static void processActionTrainDefense()
{
  int costToTrain = costTrainMultiplier * player.defense;
  if (player.gold >= costToTrain)
  {
    player.gold -= costToTrain;
    player.defense += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Trained defense.");
    updateGameWindow();
  }
  return;
}

// Function to reset the game - typically when the player has beaten it.
static void processActionResetGame()
{
  // Go back to a new player state.
  resetGame();
  updateGameWindow();
  // Show the conversation with the king!
  window_stack_push(windowConversation, true);
  return;
}

// Given an action enumeration, this function calls the appropriate function for that action.
static void processAction(int action)
{
  switch(action)
  {
    case actionNothing:
      break;
    case actionForward:
      processActionForward();
      break;
    case actionBack:
      processActionBack();
      break;
    case actionIgnore:
      processActionIgnore();
      break;
    case actionFight:
      processActionFight();
      break;
    case actionFlee:
      processActionFlee();
      break;
    case actionCastHeal:
      processActionCastHeal();
      break;
    case actionRest:
      processActionRest();
      break;
    case actionTrainAttack:
      processActionTrainAttack();
      break;
    case actionTrainDefense:
      processActionTrainDefense();
      break;
    case actionResetGame:
      processActionResetGame();
      break;
  }
  return;
}

// Event handler for when the player presses the select button at the main game screen.
static void buttonGameSelectClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionSelect() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionSelect());
  return;
}

// Event handler for when the player presses the up button at the main game screen.
static void buttonGameUpClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionUp() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionUp());
  return;
}

// Event handler for when the player presses the down button at the main game screen.
static void buttonGameDownClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionDown() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionDown());
  return;
}

// Function to register the button events for the main game screen.
static void subscribeGameWindowButtons(void *context)
{
  window_single_click_subscribe(BUTTON_ID_SELECT, buttonGameSelectClicked);
  window_single_click_subscribe(BUTTON_ID_UP, buttonGameUpClicked);
  window_single_click_subscribe(BUTTON_ID_DOWN, buttonGameDownClicked);
  return;
}

// Event handler for when the player presses the select button at the conversation screen.
static void buttonConversationSelectClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Event handler for when the player presses the up button at the conversation screen.
static void buttonConversationUpClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Event handler for when the player presses the down button at the conversation screen.
static void buttonConversationDownClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Function to register the button events for the conversation screen.
static void subscribeConversationWindowButtons(void *context)
{
  window_single_click_subscribe(BUTTON_ID_SELECT, buttonConversationSelectClicked);
  window_single_click_subscribe(BUTTON_ID_UP, buttonConversationUpClicked);
  window_single_click_subscribe(BUTTON_ID_DOWN, buttonConversationDownClicked);
}

// Event handler when the main game screen is loaded.
static void gameWindowLoad(Window *window)
{
  updateGameWindow();
  return;
}

// Event handler when the main game screen is unloaded.
static void gameWindowUnload(Window *window)
{
  return;                        
}

// Event handler when the conversation screen is loaded.
static void conversationWindowLoad(Window *window)
{
  updateConversationWindow();
  return;
}

// Event handler when the conversation screen is unloaded.
static void conversationWindowUnload(Window *window)
{
  return;                        
}

// Event handler for the main game screen canvas.
static void gameCanvasUpdate(Layer *layer, GContext *ctx)
{
  //GRect(0,80,80,20)
  //wqGPoint start = GPoint(10, 10);
  //GPoint end = GPoint(40, 60);

  // Draw a line
  //graphics_draw_line(ctx, start, end);

  // Pebble watch does not have built in floating point support, so rely on
  // integers for calculation. Using ()s around the 100 multiplication and
  // division to ensure we're not rounding out all precision.

  // Calculate how far we are to the next level.
  int experienceToNextLevel = player.experience - player.experienceLastLevel;
  int experienceForNextLevel = player.experienceNextLevel - player.experienceLastLevel;
  int percentageToNextLevel = (100 * experienceToNextLevel) / experienceForNextLevel;

  // Draw border for experience to next level.
  graphics_draw_rect(ctx, GRect(73,86,65,12));

  // Fill in how much experience we've achieve to the next level.
  int experienceBarPixels = (percentageToNextLevel * 63) / 100;
  graphics_fill_rect(ctx, GRect(74,86,experienceBarPixels,12), 0, GCornerNone);

  return;
}

// Main init call for the Pebble app!
static void init(void)
{
// Create our 2 windows: the main game window and the conversation window.
windowGame = window_create();
  windowConversation = window_create();

  window_set_click_config_provider(windowGame, subscribeGameWindowButtons);
  window_set_click_config_provider(windowConversation, subscribeConversationWindowButtons);

  strcpy(lastMessage, "");

  window_set_window_handlers(windowGame, (WindowHandlers)
                            {
                              .load = gameWindowLoad,
                              .unload = gameWindowUnload,
                            });

  window_set_window_handlers(windowConversation, (WindowHandlers)
                            {
                              .load = conversationWindowLoad,
                              .unload = conversationWindowUnload,
                            });

  //Layer *windowLayer = window_get_root_layer(windowGame);
  //GRect windowBounds = layer_get_bounds(windowLayer);


  // Create Game Layers
  GRect windowBounds = layer_get_bounds(window_get_root_layer(windowGame));
  layerGameCanvas = layer_create(windowBounds);

  // Setup the event for when the game canvas needs to be redrawn.
  layer_set_update_proc(layerGameCanvas, gameCanvasUpdate);

  // Set the locations and sizes for the many text layers on the main game screen.

  layerGameTextActionUp = text_layer_create(GRect(96,0,44,20));
  layerGameTextActionSelect = text_layer_create(GRect(66,60,74,20));
  layerGameTextActionDown = text_layer_create(GRect(96,140,44,20));

layerGameTextLocation = text_layer_create(GRect(0,0,95,20));
layerGameTextLocationDescription = text_layer_create(GRect(0,20,144,20));

  layerGameTextNpcDescription = text_layer_create(GRect(0,40,144,20));
  layerGameTextNpcHitPoints = text_layer_create(GRect(0,60,65,20));

  layerGameTextPlayerExperience = text_layer_create(GRect(0,80,70,20));

  layerGameTextPlayerAttack = text_layer_create(GRect(0,100,50,20));
  layerGameTextPlayerDefense = text_layer_create(GRect(51,100,50,20));
  layerGameTextPlayerGold = text_layer_create(GRect(102,100,42,20));

  layerGameTextPlayerHitPoints = text_layer_create(GRect(0,120,70,20));
  layerGameTextPlayerSpellPoints = text_layer_create(GRect(72,120,70,20));

  layerGameTextMessage = text_layer_create(GRect(0,140,95,20));

  // Set the locations and sizes for the many text layers on the conversation screen.

  layerConversationTextSpeaker = text_layer_create(GRect(0,0,144,20));
  layerConversationTextMessage = text_layer_create(GRect(0,20,144,128));
  layerConversationTextActionBack = text_layer_create(GRect(0,148,144,20));

  // Set the fonts and text alignments.

  text_layer_set_font(layerGameTextActionUp, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionUp, GTextAlignmentRight);

text_layer_set_font(layerGameTextActionSelect, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionSelect, GTextAlignmentRight);

text_layer_set_font(layerGameTextActionDown, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionDown, GTextAlignmentRight);

  text_layer_set_font(layerGameTextLocation, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextLocation, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextLocationDescription, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextLocationDescription, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextNpcDescription, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextNpcDescription, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextNpcHitPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextNpcHitPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerAttack, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerAttack, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerDefense, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerDefense, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerExperience, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextPlayerExperience, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerHitPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerHitPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerSpellPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerSpellPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerGold, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerGold, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextMessage, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextMessage, GTextAlignmentLeft);

  text_layer_set_font(layerConversationTextSpeaker, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerConversationTextSpeaker, GTextAlignmentCenter);

  text_layer_set_font(layerConversationTextMessage, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerConversationTextMessage, GTextAlignmentLeft);

  text_layer_set_font(layerConversationTextActionBack, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerConversationTextActionBack, GTextAlignmentLeft);

// Add the layers to the windows.

  layer_add_child(window_get_root_layer(windowGame), layerGameCanvas);

  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionUp));
  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionSelect));
  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionDown));

  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextLocation));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextLocationDescription));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextNpcDescription));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextNpcHitPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerAttack));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerDefense));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerExperience));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerHitPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerSpellPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerGold));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextMessage));

  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextSpeaker));
  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextMessage));
  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextActionBack));

  // Enable text flow and paging on the text layer, with a slight inset of 10, for round screens
  //text_layer_enable_screen_text_flow_and_paging(layerGameTextLocation, 10);

  bool showIntro = false;

  // Attempt to load an existing game.
  if (!loadGame())
  {
    // Load failed, reset the player.
    resetGame();
 
    showIntro = true;
  }

// Push the main game window with the window animation.
window_stack_push(windowGame, true);

  // If this is the first time we're starting the game on this Pebble, we'll show the
  // intro (which is just the king telling us what he wants).
  if (showIntro)
  {
    window_stack_push(windowConversation, true);
  }

// App Logging!
//APP_LOG(APP_LOG_LEVEL_DEBUG, "Just pushed a window!");

  return;
}

// Main closing function. Saves the game and cleans up variables from memory.
static void deinit(void)
{
  // Save the progress the player has done.
  saveGame();

// Destroy the text layers.
  text_layer_destroy(layerGameTextActionUp);
  text_layer_destroy(layerGameTextActionSelect);
  text_layer_destroy(layerGameTextActionDown);
text_layer_destroy(layerGameTextLocation);
text_layer_destroy(layerGameTextLocationDescription);
text_layer_destroy(layerGameTextNpcDescription);
text_layer_destroy(layerGameTextNpcHitPoints);
text_layer_destroy(layerGameTextPlayerAttack);
text_layer_destroy(layerGameTextPlayerDefense);
text_layer_destroy(layerGameTextPlayerExperience);
text_layer_destroy(layerGameTextPlayerHitPoints);
text_layer_destroy(layerGameTextPlayerSpellPoints);
text_layer_destroy(layerGameTextPlayerGold);
text_layer_destroy(layerGameTextMessage);

  text_layer_destroy(layerConversationTextSpeaker);
  text_layer_destroy(layerConversationTextMessage);
  text_layer_destroy(layerConversationTextActionBack);

// Destroy the windows.
window_destroy(windowGame);
  window_destroy(windowConversation);

  return;
}

// Last but not least, the main function!
int main(void)
{
init();
app_event_loop();
deinit();
}


Copyright (c) 2016 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

No comments:

Post a Comment