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.

Thursday, September 8, 2016

Office / Shop: Reinforcement

Progress continues on the shop building - an additional beam keeps the new wall stable, the new wall has gotten plywood on one side, and the roof purlins have been doubled-up for more strength.

Support Beam


I was originally going to put in a ceiling at 9', but I've now decided to open the room up all the way to the existing roof structure. That necessitated a few changes, but ultimately it will be better.

The new wall has many bolts tying it into the concrete floor at the base, and it's securely attached to columns on either end, but it's spanning 24 feet so I wanted something to brace it in the middle.

The new bracing beam is a double 2x8.


On one end it's attached to the center column of the exterior wall (not going anywhere).


I do have blocking in the middle to keep it from twisting. In addition to the structural support, this will be a great mount for lights.



Plywood Wall


Next up was to get plywood on at least one side of the wall. I'm trying to get everything organized and having 24' of finished wall makes a huge difference.


Already started putting up shelves on the finished side...


Roof Reinforcement


Now that I'm opening my room up to the existing roof, I want to reinforce its structure so it doesn't sag with weight of insulation, plywood, and solar panels. This is definitely overkill, but I doubled up every 2x6 purlin.


Also cleaned out the room; get it ready for some floor work!


Wow, I just noticed how well you can see the light rays coming from the window to the floor. I had just finished sweeping so there was plenty of dust in the air. :-)


Future


I'll have to add a cat door at some point.


I'm narrowing down my solar panel system purchase. The goal will be to have a small system in place by this winter for emergency power. (And it will be a good test to see how well the panels perform with the short winter days.)

Tuesday, September 6, 2016

2013 Ford Focus Electric: Range Update

The all electric Ford Focus is still running great.

Some updates on the range...


Recent photo, 95 miles!

Speed


The 95 miles was after some heavy stop and go traffic driving home during Friday rush hour. If you're typically commuting in medium to heavy traffic, you're going to be well within the advertised range of the car. The car really shines at lower speeds. If you typically cruise 75+ to and from work, you won't be reaching the advertised range.

It's 60 miles one-way to the airport for me - essentially all highway driving, and I charge my car there. (I've never had a problem getting an outlet to charge from.) I try to keep the climate controls off / minimal for that long trip, and I don't speed. Range is much better driving 55 vs 65. If I don't hit traffic, I usually arrive with about 10 miles remaining. With heavy traffic I've had 30 miles estimated range upon arriving.

Automatic Climate Control


Cold has an effect on range, but running the heater to warm up the cabin really has an effect on the range. As in I'll easily see the range drop 10-15% when the heater is operating. If it's not too cold, I just max out the seat heaters which doesn't even budge the range.

You would think running the air conditioner would have a similar major effect on the range. Surprisingly, that's not always the case. If it's in the upper 70s outside, setting the temperature to 72 in the car may reduce the range 15%. Adjusting the temperature down to 68, and the range actually increases with an overall reduction of only 3-4%! After some Googling, it sounds like the car will actually engage the heater alongside the A/C to modulate the temperature; my personal experiences match what they're saying.

So the best options seem to be...

  • If it's hot outside, run the A/C.
  • If your target temperature is at/near ambient temperature, disable automatic climate control (set the temperature to "Low" or "High" and manually control the fan) or crack the windows and enjoy the weather.
  • If it's cool outside, just run the seat warmers.
  • If it's cold outside, run the heater.

Cold Weather ** UPDATE 20 Jan 2017 **


So I'm in my first real winter with the electric car (Texas winters don't count). Cold weather definitely has an effect on the range. With the temperature at freezing and heater off, the car's range in miles reports in the low 60s. If I turn the heater on, the car's range has dropped to as low as 42 miles! If I need to go over 40 miles on those cold days, I'll periodically cycle the defroster on/off manually while driving just to keep the windshield clear.

Battery Life


The car is 3 years old now, and I haven't noticed any appreciable degradation of the battery. The airport drive is a good test as it uses the bulk of the range; it will be interesting to see how it performs next year!


Range Anxiety


I did have "range anxiety" for the first couple weeks of owning the car. (Basically worrying if I really could make it to my destination and back.) That hasn't been a problem though for a long time. I check the car's range before driving, but I've never run out of juice or had to make an emergency stop to charge up.

Thursday, August 18, 2016

Pebble Game: 2 Steps RPG

I've had an itch to create a role playing game on my Pebble watch for a while now. There are several RPGs available for the watch (some quite nice), but overall I think they tried to shoehorn a game meant to be played on a computer platform (with the vast input devices available) into a watch with 4 buttons.

I wanted the game to embrace the watch platform and be designed specifically for it.

My design methodology:

  • Movement in the game should be forward or back. In RPGs, you're really just going to a quest goal (with more danger) or coming back home (to train/rest/etc). Unless you're trying to replicate a maze, there's no need to be able to turn left or right. So I threw out the ability to turn! Now I only need 2 buttons for the entire movement system: "forward" and "back".
  • As you're heading towards your quest goal, you'll encounter enemies along the way that will block your path. When blocked there is no need for a "forward" action, so we'll just replace that with an "attack". Not only does this double up buttons on our very limited user-interface, it also allows the player to plow through easier enemies without moving their fingers over. Just keep pressing the same button and you'll either move forward or kill what's in front of you then move forward.
  • The same is true for the reverse movement. If you encounter an enemy too difficult, then you want to go back. To not make the game too easy, once you're in combat you can't just walk away without consequences; you have to attempt to flee which could result in getting hurt. So the "back" action gets replaced with a "flee". Again if you just want to go home, you can keep pressing the same button and you'll either move back or flee the combat to move back without moving your fingers over. That doubles up another button on the user interface.
  • It bugs me when there are killer squirrels in RPGs that chase you down and gnaw at your ankles until you collapse and die. In reality if you're trying to kill a squirrel then decide to walk away and ignore him, the squirrel is more than happy to let you walk away. So some monsters require you to attempt to "flee" from combat while others allow you to just "ignore" them. "Ignore" is 100% successful, and the monster doesn't try to take one last bite out of you as you walk away.
  • Pretty much any RPG is going to have some kind of economy. The player goes out, kills a rabbit, then sells its fur for a little cash. The player goes out, kills a skeleton, then sells the sword it was carrying for cash. But it's all the same - kill, put in inventory, go back to town, sell for money. We'll cut out the middle steps - when you kill something we'll just give you the money as though you instantly sold it. You can't use the cash until you go back to town anyways, so what does it matter that you go back to town with 10 rabbit furs or $10? I didn't implement encumbrance, but it could be simulated by putting a cap on how much gold you carry at any one time.
  • Being a watch app, it needs to be very quick and easy for the player to leave the game. With only a single exception (that I'll cover next), the back button on the Pebble watch needs to immediately exit the game and go back to the watchface. Done playing this session? Hit one button and you're out!
  • The majority of the game is on a single screen, but there are a handful of major events that require a fair amount of text so the player knows what is going on. On those screens, the up/down/select buttons on the watch do nothing - the player must press "back". This ensures the player won't accidentally skip an important conversation while they're plowing to or fleeing from enemies. So the conversation window comes up, read it, press "back", and you're back in the game. Accidentally double press "back" and go to the watchface? No problem! Just jump back in the game right where you left off, which leads to my next point.
  • This is a game on a watch - people aren't going to stare at this tiny screen for 30 minutes. They're going to play in very small doses at sporadic times of opportunity. It must be seamless to leave the game and go back in right where they left off. Upon exiting, the game automatically saves the entire state (game settings, player properties, and current enemy properties). Upon startup, the game automatically loads the entire state - including if you're in the middle of combat.
  • So that leaves 1 button, "select". To be most valuable, it needs to be context sensitive. If you're in the Inn then likely you want to heal up, so we'll make that button "rest". If you're at the trainer then likely you want to train, so we'll make that button "train". If you're in (or just out of) combat then likely you want to regenerate your health, so we'll make that button "cast heal". The first cut of the game only supports the one spell "cast heal". I can see future versions of the game having a variety of spells or equipment items that would be accessed under the "select" button (which would bring up a list to pick from). That will mean a few extra button presses for those special actions, but it won't be something the player constantly has to do.
  • Although the user interface is straightforward, game actions are always labeled next to the buttons. For one thing, users don't read manuals. If they have to read a manual to play a little game on their watch then they won't play it (and I can't blame them). Tutorials that teach them the controls the first time they play aren't good either. What happens when you stop playing for 3 weeks? You forget all your controls and you're not going through the tutorial all over again.
  • Finally, I wanted the Pebble watch to have a game you could beat. Most Pebble games I played just had you maximize your score then try again, but that's not as satisfying as being able to say I BEAT this game. The game as designed has some replayability - when you win it tells you how many moves it took (and records your fewest moves) so you could try and win again in even fewer moves. Or you could try beating it by training only attack or training only defense or never training anything or never dying. If I make a version 2, I'll include more spells so you could try winning as a spell caster or fighter or combination of the two.


A poster on the Pebble discussion board offered a great name for the game that I'm using "2 Steps". As in you're going to be doing a lot of 2 steps forward only to go 1 step back.

Screenshot time!

Game Intro

Inn to Rest

Defense Trainer

In Combat


I still only have the original black and white Pebble; a color Pebble will give me lots more options on the user interface. The next version of the game will likely take advantage of color to consolidate status bars, have a background color scheme relevant to the area you're in, and just be prettier in general. (I've already been asked to add images for each of the creatures.) The name of the sequel is too obvious to not use "2 Steps: Beyond the Gate". ;-)

I plan on releasing the source code after I get time to more thoroughly document it. Stay tuned for that post.

UPDATE: Code published to: http://clintkprojects.blogspot.com/2016/10/pebble-game-2-steps-rpg-source-code.html

Sunday, July 17, 2016

Office / Shop: Exterior Wall

The exterior wall is complete! I'm not a fan of toothpaste blue, so I'll eventually repaint the entire building. In the mean time though it is water tight and secure!

Frame...


Plywood...


Wrap...



Door...



Siding (metal roofing)...