Combat in TADS3

Hi folks. I’ve began learning TADS 3, and I must say, it’s an interesting language. However, I have a
question; is there anything like Steve Breslin’s T2Combat for TADS3? I haven’t found anything yet.

I have a game in Beta that uses a more complex system than that. Right now many of the methods are just place-holders for future development on my combat system. I’ll share a little of what I have so far, just to give you an idea of how complicated this stuff can get:

/*
 *   + this object below a defined Monster and factions mentioned in it will
 *     adjust automatically on monster death. Use this only on Monster class
 *     or Person class.
 *
 *   Three things you must setup on this object are:
 *      factionGroup
 *      parent // optional
 *      factionHit 
 */
class FactionAdjustOnDeath: object
   factionGroup = nil // set this to the FactionGroup object
   factionHit = -2 // override for bigger faction hits , positive increases faction, negative decreases
   onDeathScript(killer){  // classes must override
      if(killer != nil) self.notifyFaction(killer);         
   }
   notifyFaction(killer){
      if(self.factionGroup==nil) exit;
      if(!self.factionGroup.ofKind(FactionGroup)) exit;
      factionGroup.takeFactionHit(self,killer);
   }  
;

// class Monster: Person
// changed class to AdvancedCromexxHuman on 1JUN2014 to reflect that Monsters have statistics for resist checks
class Monster: Person
    desc {
        if(gameMain.showImages() && (self.myPic != nil)){
                     gameMain.displayMonsterPic(self.myPic);
        }
        "<<myDesc>>";
        local restedHP = self.getFullyRestedHP();
        local currHP = self.getCurrHP();
        local iDiff = new BigNumber((new BigNumber(restedHP)) - currHP);
        local absDiff = new BigNumber(iDiff);
        absDiff = absDiff.getAbs();
            // (current / rested) * 100 
        local mystat = ((
                       (new BigNumber(currHP)) / (new BigNumber(restedHP)) 
                      ) * 100);
        local hBar = self.myHPMeter(mystat);// number from 1 to 100
        "<br><<hBar>>";
    }
  myPic = nil 
  myDesc = 'A simple monster. ' // override
  stats = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] // 18 of them
  statNames = ['str','int','dex','agi','wis','cha','con','sta','nature','death','fire','cold','magic','poison','disease','divine','wil','end']
  notifyXPReward(points,monster){
    self.xp+=points;// should modify formula based on monster level
    self.xpBar+=points;
    local iLevelup = gameMain.levelXP[self.level];// grab the total XP required to advance to next level
    if(xpBar>=iLevelup){
       "{The self/him} levels up! ";
       self.level++;
       if(self==gPlayerChar) "Welcome to level <<self.level>> ! ";
       local iDiff=self.xpBar-iLevelup;// grab the difference
       if(iDiff<=0) iDiff=0;
       self.xpBar=iDiff;// reset the xp bar and add the difference (if any) towards the next level
    }else{
       "<br><font color=yellow>{You/he} get{s} <<points>> points of experience!</font> ";
    }
  }
  level = 2 // default is 1 level above a starting character
  factionAdjustOndeathList = [] // list of all FactionAdjustOnDeath objects to fire off when monster dies
  enemyList = [] // list of enemies to attack in this order
  lastAttacker = nil // last Actor who attacked this monster
  preferredWeapon = nil // this monster's preferred weapon to use
  kicker = nil // can this monster kick the attacker?
  puncher= nil // can this monster punch the attacker?
  biter = nil // can this monster bite the attacker?
  clawer = nil // can this monster claw the attacker?
  basher=nil // can this monster use- "bash" (to stun)?
  breathweaponEnabled = nil // can this monster use a breath weapon? (if so we can simply cycle through the Weapon class object list in contents)
  magicuser = nil // is this monster a magic user?
  spellList = [] // list of spells this monster can use
  counterAttack=nil // if attacked can this monster use counter-attack right away as a free attack?
  dualWield=nil // can this monster dual-wield weapons?
  fleer=true // monsters are able to flee battle by default
  fleepoint=nil // this should be a Room object where the monster will flee to if fleeing
  offhand = nil // weapon in offhand
  mainhand = nil // weapon in main hand
  // replies to a chat tell command
  chatReply(sentstring){ 
        "<br>You can\'t use chat commands to communicate with monsters! ";
  }
  checkAggro(nearbyactor){
     if(self.factionGroup!=nil){
        if(!self.factionGroup.ofKind(FactionGroup)) return nil;       
        return self.factionGroup.verifyAggro(nearbyactor);
     }else{
        return nil;
     }
  }
  notifyFaction(killer){
      foreach(local obj in self.factionAdjustOndeathList){
          if(obj != nil){
              if(obj.ofKind(FactionAdjustOnDeath)){
                 obj.onDeathScript(killer);
              }
          }
      }
  }
  dobjFor(Attack){
    verify(){ logicalRank(110,'likely'); }
    check(){ }
    action() { askForIobj(AttackWith); }
  }
  fleeMsg = nil
  dobjFor(AttackWith){
    verify(){ logicalRank(110,'likely'); }
    check(){ }
    action(){ 
          //if(gIobj!=nil){
          //  "gIobj!=nil!!!!! and its name is: <<gIobj.name>> !!!";
          //}
          if(!gIobj.ofKind(Weapon)){
               "You can\'t attack anything with that. ";
               exit;
          }   
          // 1. determine hit or miss
          // 2. apply damage if needed
          // gDobj.applyDamage(gIobj.getDamage(),gActor);  
         
          // Below is obsolete - attach an AttackEnemy agenda item to the monster to handle attacks 
          //  gDobj.applyDamage(gIobj.getDamageComplex(gPlayerChar,gActor),gActor);
    }    
  }
  calculateReward(attacker){     
     return self.xpReward; // each class can modify
  }
  moneyReward = nil // if true then check the following: goldReward, silverReward, copperReward
  goldReward = 0
  silverReward = 0
  copperReward = 0
  itemsReward = []
  rewardedMonty = nil
  xpReward = 10 // overrride to increase experience point reward
  hitpoints = 20 // override to increase or decrease as needed
  baseHitpoints = 20 // <-- ALL monster classes MUST use this for valid HP bar display on an "examine monster" command
    
  /*
   *  This is an internal method only. Do not call this directly.
   *  Various monster classes may override this to change defenses 
   *  based on what attacks they perceive (through this notification).
   *
   *  The player will most likely receive the default attackString showing 
   *  a missed attack.
   *  
   *  iAttackType is an integer from 0 to 11.
   *     0 = unknown
   *     1 = melee (includes: bite, claw, punch, kick)
   *     2 = blunt
   *     3 = piercing (includes: spears, javelins)
   *     4 = stabbing (includes: swords, daggers)
   *     5 = slicing (includes: axes, halberds, swords, daggers, etc.)
   *     6 = nature magic
   *     7 = death magic
   *     8 = fire magic
   *     9 = cold magic
   *    10 = general magical attack
   *    11 = poison
   *
   */
  receiveMissedAttackNofication(attacker,iAttackType,attackString){
        if(iAttackType == nil) iAttackType = 0;
        if(self == gPlayerChar) say(attackString);
  }

    rewardedMoney = nil
    
  /* 
   * This (self) creature or person TAKES damage from attacker and shows it damageString
   *
   * NOTE: Do not call this method directly. It is called from dealDamage method as an internal
   *       notifier from attacker to victim.
   */   
    // 30MAY2014 - this is where to edit this function. WDC
  takeDamage(damage,attacker,damageString){
         // *** To do: check resists before just taking straight damage
         
         self.hitpoints -= damage;
         local s0 = damageString;
         if(self.hitpoints <=0){
              self.hitpoints = 0;
              s0 += '<br>\^' + self.name + ' dies! Its corpse evaporates in an orange cloud of smoke. ';
              say(s0);
              if(self.xpReward>0){              
                   attacker.notifyXPReward(self.calculateReward(attacker),self);
              }
              self.notifyFaction(attacker);
            
              self.moveInto(pixyland);   
              if(self.moneyReward){
                 // checking rewardedMoney because for some reason script was firing off TWICE!
                 if((attacker == gPlayerChar) && (!self.rewardedMoney)){
                        //local playerGold = new BigNumber(gameMain.myCarriedGold.getAbs()).getFloor();
                        //local playerSilver = new BigNumber(gameMain.myCarriedSilver.getAbs()).getFloor();
                        //local playerCopper = new BigNumber(gameMain.myCarriedCopper.getAbs()).getFloor();           
                        // monster reward
                        //playerGold += self.goldReward;
                        //playerSilver += self.silverReward;
                        //playerCopper += self.copperReward;
                    
                    gameMain.myCarriedCopper += self.copperReward;
                    gameMain.myCarriedSilver += self.silverReward;
                    gameMain.myCarriedGold += self.goldReward;
                    "<br>You take <<self.copperReward>>c <<self.silverReward>>s <<self.goldReward>>g from the evaporated remains. ";
                  }
                  self.rewardedMoney = true;
              }
         }else{
              say(s0);     
         }         
  }
  /*
   * This (self) creature or person DEALS damage to a target, showing damageString + generic damage message
   *
   * iDamageType is an integer from 0 to 11.
   *     0 = unknown
   *     1 = melee (includes: bite, claw, punch, kick)
   *     2 = blunt
   *     3 = piercing (includes: spears, javelins)
   *     4 = stabbing (includes: swords, daggers)
   *     5 = slicing (includes: axes, halberds, swords, daggers, etc.)
   *     6 = nature magic
   *     7 = death magic
   *     8 = fire magic
   *     9 = cold magic
   *    10 = general magical attack
   *    11 = poison
   *
   */
  dealDamage(damage,iDamageType,target,damageString){
       local s0 = damageString;
       if(damage==nil) damage=0;
       if(damage==0) exit; // do nothing if no damage to apply
       self.lastAttacker=target;
        s0 += '<table><tr><td bgcolor=black>';
       local sName = (target == gPlayerChar) ? 'You' : target.name;
       s0 += '<font color=#00FFFF>...\^' + sName + ' take';
       s0 += (target != gPlayerChar) ? 's' : ' ';
       s0 += ' ' + toString(damage) + ' point';
       s0 += (damage > 1) ? 's damage' : ' damage';
       s0 += '!</font></td></tr></table><.p>';      

       // is the attacker who attacked us in our enemy list? if not, add them!
       if(self.enemyList!=nil){
          local iMatch = nil;
          foreach(local oEnemy in self.enemyList){
             if(oEnemy==target) iMatch=true;
          }
          if(iMatch==nil) self.enemyList+=target;
       }else{
          self.enemyList+=target;
       }
       // notify target so it can take damage
       target.takeDamage(damage,self,s0);
  }
  tryBuildingConnector(conn,exitList){
     if(conn!=nil){
            if((!conn.ofKind(NoTravelMessage)) &&
               (!conn.ofKind(FakeConnector)))
                 exitList+=conn;
     }
     return exitList;
  }
  kickAttack(target){ } // override these as necessary - this is where the monster decides to attack "target" in a certain way
  punchAttack(target){ } // override
  biteAttack(target){ } // override
  clawAttack(target){ }
  breathAttack(target){ }
  spellAttack(target){ } // spell cast from spells located in self.spellList[] 
  sayAttackMsg = true  // or nil to NOT say the attackMsg on first attack
  attackMsg = '...Grrrrrr!'    
  saidAttackMsg = nil // or true if we already said it
  weildedAttack(target,mainweap,offhand){ 
        // attack target with main weapon of mainweap, and offhand weapon if self can dual wield.
        if(mainweap==nil) exit;// if we have no primary weapon then exit
        if(target==nil) exit;// if no target then nothing to attack, so exit
        local s0 = '';
        // say the initial attack message?
        s0 += '<table><tr><td bgcolor=black>';
        if((target == gPlayerChar) && (self.sayAttackMsg==true) && (saidAttackMsg == nil)){
            s0 += '<font color=#00FFFF>' + self.attackMsg + '</font><br>';
            self.saidAttackMsg = true;
        }
        s0+='<font color=#00FFFF>\^' + self.name + ' ';
        // add in a random attack word 'stabs','slashes', 'slices', 'swings', etc.
        s0 += mainweap.attackWords[rand(mainweap.attackWords.length)+1];
        s0 += '...</font> ';   
        s0 += '<font color=#00FFFF>...';
        local iDmg = mainweap.getDamageComplex(self,target);
        if(
           ( iDmg == 0 ) || 
           ( mainweap.calculateHitMiss(self,target)==nil ) 
          ){
              s0+='but misses!</font><.p> ';
              s0+='</td></tr></table>';
              target.receiveMissedAttackNofication(self,3,s0);
        }else{
              // auto hit based on DEX (attacker) vs. AGI (defender) (calculates auto-advantage)
              // or dmg passed from getDamageComplex
            
            
              local sName = (target == gPlayerChar) ? 'YOU' : target.name;
              s0+=' and hits \^' + sName + ' for ';
              s0=s0 + toString(iDmg) + ' damage!</font><.p>';      
              s0+='</td></tr></table>';
              self.dealDamage(iDmg,3,target,s0);// ... which then calls target.takeDamage method
        }
  }
  //topicInventoryDaemon(){
  //     inherited;
  // }
  // what this monster does on an attack turn IF an AttackEnemy agenda item is active
  attackTurn(enemyList){
       // is the monster dead or at a nil location? if so abort
       if((hitpoints<=0) || (location==nil)) exit;
       if(enemyList==nil) exit; 
       if(enemyList.length==0) exit;
       // clear whatever's not here that's in our aggro list
        foreach(local obj in enemyList){
            if(!canSee(obj)){
                enemyList -= obj;
            }
        }
       // see if we can spot someone we don't like who's not on our list yet
       local iAggro=nil;
       foreach(local oActor in self.getOutermostRoom.contents){ 
           if(oActor.ofKind(Actor)){
             iAggro = checkAggro(oActor);
             if(iAggro!=nil) enemyList+=oActor;// may aggro on next turn now!
           }
       }
       //local oWeapon = self;// the actual physical Weapon class object we will attack with if we have one 
       // "<br>Getting nominal target to attack...";
       //local oFirst = self;
       //foreach(local oEnemy in enemyList){
       //    if(oFirst==nil) oFirst=oEnemy;//  "<br><font color=white><<oEnemy.name>></font> ";
       //}

       // is the player NOT at the monster location - note: override for monsters that chance the player
       // if(oFirst == nil) exit;// do nothing is no first enemy found here
       
       // default behavior of monsters is to NOT charge after the player - ovveride if you want this handled differently
        // also monsters don't attack things if the player's not there
        if(gActor != nil){
            if(gActor.enemyList[1] != nil){
                if(gActor.canSee(gPlayerChar) == nil){
                    exit;
                }else if(!gActor.canSee(gPlayerChar)){
                    exit;
                }
            }
        }// do nothing if target not at monster location

       // "<br>Getting preferred weapon to attack <<oFirst.name>> with... ";
       if(preferredWeapon!=nil){
                  // say(preferredWeapon.name); // preferredWeapon is deprecated  
                  //"Do something.... ";
                  // randomly attack someone in our list
                  if((dualWield!=nil) && (offhand!=nil)){
                       weildedAttack(enemyList[rand(enemyList.length)+1],mainhand,offhand);
                  }else{
                       weildedAttack(enemyList[rand(enemyList.length)+1],mainhand,nil);
                  }
       }else{
            // no preferred weapon so we can randomly use any weapon at our disposal - this is default for most monsters
            local atkType;
            local iDone=0;
            local iFailsafe=0;
            local tokList;
            local oExit;
            do{
              iFailsafe++;
              if(iFailsafe>50) iDone=1;
              atkType=rand(9)+1;
              switch(atkType){
                case 1:
                  if(self.kicker!=nil) iDone=1;
                  break;
                case 2:
                  if(self.puncher!=nil) iDone=1;
                  break;
                case 3:
                  if(self.biter!=nil) iDone=1;
                  break;
                case 4:
                  if(self.clawer!=nil) iDone=1;
                  break;
                case 5:
                  if(self.basher!=nil) iDone=1;
                  break;
                case 6:
                  if(self.breathweaponEnabled!=nil) iDone=1;
                  break;
                case 7:
                  if(self.magicuser!=nil) iDone=1;
                  break;
                default: 
                  foreach(local o in self.contents){
                      if(o.ofKind(Weapon)) iDone=1;                      
                  }
                  //   weapon-in-hand - the default
              }
            }while(iDone==0);
            // ** now handle the attack
            if(iFailsafe>50){
                  // the monster could not find anything to attack the target with! So run away if we can!
                  if(self.fleer==nil) exit;// can not flee so exit
                  if(self.fleepoint!=nil){

                  }else{
                       local oExitList=[];
                       local iResolved=0;
                       // no flee point found, so lets find a random exit and use it
                       if(self.location.east!=nil) oExitList=tryBuildingConnector(self.location.east,oExitList);
                       if(self.location.west!=nil) oExitList=tryBuildingConnector(self.location.west,oExitList);
                       if(self.location.north!=nil) oExitList=tryBuildingConnector(self.location.north,oExitList);
                       if(self.location.south!=nil) oExitList=tryBuildingConnector(self.location.south,oExitList);
                       if(self.location.northwest!=nil) oExitList=tryBuildingConnector(self.location.northwest,oExitList);
                       if(self.location.southwest!=nil) oExitList=tryBuildingConnector(self.location.southwest,oExitList);
                       if(self.location.northeast!=nil) oExitList=tryBuildingConnector(self.location.northeast,oExitList);
                       if(self.location.southeast!=nil) oExitList=tryBuildingConnector(self.location.southeast,oExitList);
                       if(self.location.up!=nil) oExitList=tryBuildingConnector(self.location.up,oExitList);
                       if(self.location.down!=nil) oExitList=tryBuildingConnector(self.location.down,oExitList);
                       if(self.location.in!=nil) oExitList=tryBuildingConnector(self.location.in,oExitList);
                       if(self.location.out!=nil) oExitList=tryBuildingConnector(self.location.out,oExitList);   
                       local iRand = rand(oExitList.length)+1;
                       if(iRand>oExitList.length) iRand=1;
                       if(oExitList.length<=0) exit;
                       tokList = Tokenizer.tokenize('wait');
                       oExit=oExitList[iRand];
                       if(oExit.ofKind(Door)){
                         tokList = Tokenizer.tokenize('enter ' + oExitList[iRand].name);
                         iResolved=1;
                       }else if(oExit.ofKind(FakeConnector)){
                       }else if(oExit.ofKind(Enterable)){
                         tokList = Tokenizer.tokenize('enter ' + oExitList[iRand].name);
                         iResolved=1;
                       }else if(oExit.ofKind(Exitable)){
                         tokList = Tokenizer.tokenize('exit ' + oExitList[iRand].name);
                         iResolved=1;
                       }else if(oExit.ofKind(NoTravelMessage)){
                       }else{
                         //tokList = Tokenizer.tokenize('go ' + oExitList[iRand]);
                         iResolved=2;// must be a directional exit
                       }
                       if(iResolved==0){
                         "Resolved is zero!<br><br> ";
                          executeCommand(self,self,tokList,nil);// nil = start of sentence (true/nil)
                       }else if(iResolved==2){
                          // find an exit...
                          iRand=rand(oExitList.length)+1;
                          if(iRand>oExitList.length) iRand=1;
                          if(oExitList.length!=0){  
                            newActorAction(gActor,Enter,oExitList[iRand]);
                            if(gActor.fleeMsg.ofKind(Script)){
                              gActor.fleeMsg.doScript();
                            }else{
                              gActor.fleeMsg();
                            }
                          }
                       }else{
                           if(oExitList.length>0){
                              newActorAction(gActor,Enter,oExitList[1]); // nestedActorAction
                           }
                       }
                 }                                  
            }else{
               // fail-safe above
               switch(atkType){
                case 1:
                  "Kick! ";
                  break;
                case 2:
                  "Punch! ";
                  break;
                case 3:
                  "Bite! ";
                  break;
                case 4:
                  "Claw! ";
                  break;
                case 5:
                  "Bash! ";
                  break;
                case 6:
                  "Breath! ";
                  break;
                case 7:
                  "Spellcast! ";
                  break;
                default: 
                  if(mainhand==nil) exit; // do nothing if no main weapon
                  // randomly attack someone in our list
                  if((dualWield!=nil) && (offhand!=nil)){
                       weildedAttack(enemyList[rand(enemyList.length)+1],mainhand,offhand);
                  }else{
                       weildedAttack(enemyList[rand(enemyList.length)+1],mainhand,nil);
                  }                 
              }

            }
            
       }
  }
    
    /*
     *
            // ** calculate current hitpoint bar
            local restedHP = gPlayerChar.getFullyRestedHP();
            local currHP = gPlayerChar.getCurrHP();
            iDiff = new BigNumber((new BigNumber(restedHP)) - currHP);
            absDiff = new BigNumber(iDiff);
            absDiff = absDiff.getAbs();
            // (current / rested) * 100 
            mystat = ((
                       (new BigNumber(currHP)) / (new BigNumber(restedHP)) 
                      ) * 100);
            local hBar =  gPlayerChar.myHPMeter(mystat);// number from 1 to 100

     *
     */
    
    getCurrHP(){
       // 1. get base hitpoints
       // local baseHP = self.baseHitpoints;
       // 2. get current hitpoints
       local currHP = self.hitpoints;
       // 3. TO DO: loop through all hitpoint (addition) spell effects on the gActor (i.e. regen, temp hitpoints, etc.)
       local spellEffectHP = 0;
       // 4. TO DO: loop through all *negative* (curse) hitpoint spell effects on gActor
       local negativeHP = 0;
       // 5. TO DO: loop through all clothing (addition) spell effects on the gActor (i.e. regen, added hitpoints, etc.)
       local wornHP = 0;
       local returnHP = currHP + spellEffectHP + negativeHP + wornHP;
       return returnHP;
  }
  // grab fullest hitpoints player can have based on fully rested HP
  getFullyRestedHP(){
       // 1. get base hitpoints       
       // local baseHP = self.baseHitpoints;
       // 2. TO DO: loop through all hitpoint (addition) spell effects on the gActor (i.e. regen, temp hitpoints, etc.)
       local spellEffectHP = 0;
       // 4. TO DO: loop through all clothing (addition) spell effects on the gActor (i.e. regen, added hitpoints, etc.)
       local wornHP = 0;
       local returnHP = self.baseHitpoints + spellEffectHP + wornHP;
       return returnHP;
   }

    
    // must be 1-100 - XP bar style meter - no frills. works
    myHPMeter(mystat){
        local s0='';
        local i = 1;
        local emptyBub = '<img src=\"web/img/hp_gray.png\" height=5 width=10 border=1>';
        local fullBub = '<img src=\"web/img/hp_bubble.png\" height=5 width=10 border=1>';
        local quarterBub = '<img src=\"web/img/hp_bub25.png\" height=5 width=10 border=1>';// 25 percent of a bubble
        local halfBub = '<img src=\"web/img/hp_bub50.png\" height=5 width=10 border=1>';// 50 percent of a bubble
        local threequarterBub = '<img src=\"web/img/hp_bub75.png\" height=5 width=10 border=1>';// 75 percent of a bubble
        for(i=0;i < 5;i++){            
            if(mystat == 0){
                 s0 += emptyBub;                  
            }else if(mystat < (i * 20)) {
                 // mystat was in a previous bubble so this one must be empty
                 s0 += emptyBub;
            }else{
                 // must be fractional amount of this bubble 
                 if(mystat < ( (i * 20) + 5) ){                                          
                     s0 += quarterBub;
                 }else if(mystat < ( (i * 20) + 10) ){
                     s0 += halfBub;
                 }else if(mystat < ( (i * 20) + 15) ){
                     s0 += threequarterBub;
                 }else{
                     // unreachable ?
                     s0 += fullBub;
                 }
            }
        }// end for
        return s0;
    }
  
;

class AttackEnemy : AgendaItem 
  initiallyActive = true 
  isReady = true 
  isActive = true
  attacker = nil // gPlayerChar
  alwaysAggro = nil
  agendaOrder = 1
  invokeItem  { 
        // local myActor = getActor();// the Actor whom this agenda item belongs
        local iAggro=nil;
        if(alwaysAggro){
            if((gActor.canSee(gPlayerChar)) && (gActor.enemyList.length > 0)){
                if(gActor.enemyList.indexOf(gPlayerChar) == nil){
                    gActor.enemyList += gPlayerChar;
                }
            }
        }
        if(gActor.location==nil) exit;// if we are at nil location do nothing
        foreach(local oActor in gActor.location.contents){ 
           if(oActor.ofKind(Actor)){
             iAggro = gActor.checkAggro(oActor);
             if(iAggro!=nil) gActor.enemyList+=oActor;// may aggro on next turn now!
           }
        }
    if((gActor.ofKind(Monster)) && (gActor.enemyList!=nil)){
        gActor.attackTurn(gActor.enemyList);
    }
  } 
;

/*
 * NOTE: my AdvancedClothing class isn't listed here (it just gets more and more compliated and you really can just have
 *          the Weapon class extend Thing class instead.
 */
class Weapon: AdvancedClothing
  price = 0
  sellPrice = 0
  damage(target){
     self.getDamage();
  }
        /*
         *   Inventory numbers for weapons are: 
         *   6 right hand (weapon) 
         *   14 left hand (weapon) - usually decided at #6 but generally used for
         *   shields
         */
    clothLevel = 14 // right handed weapon by default
  /*
   * Weapon: So any of these DEALS modifiers to a statistic based attack (willpower for example)
   *         AND DEFENDS weilder/wearer against stat rolls vs. the statBonus stat.
   */
  specialAttack1 = 0
  specialAttack2 = 0
  specialAttack3 = 0
  specialAttackName1 = '' // what's shown in the description <table> as the name for the spec. attack. i.e. 'Vampiric', 
  specialAttackName2 = ''
  /*
   *   These are called directly from the AttackWith method
   */
  specialAttackMethod1(){  // classes must override
                         }
  specialAttackMethod2(){ }
  specialAttackMethod3(){ }   
  spellEffects = [] // spell objects (both good and bad, buffs and curses) go in this list and stats are modified on the fly based on statbuffs
     
  // override per weapon type - this sends back strings of attack words. default is slashing attack
  attackWords = ['slashes ','swings ','stabs ','parries ','thrusts ','slices ']
  alwaysHit = nil // overrides random chance to hit - we may want this on easy weapons and monsters
  hitMsg = '{You/he} swing{s} the weapon and hit{s} the {dobj/him}. '
  missMsg = '{You/he} swing{s} and miss. '
    
    /*
     *   twentyDieRoll(statistic,fontColor){ if(fontColor == nil) fontColor =
     *   '#58D3F7';// light blue local iRandy = rand(20)+1; if(statistic == nil)
     *   statistic = 1; local iStatMod = getStatModifier(statistic); local
     *   iStatBase = stats[statistic]; local iStatModded = iStatMod + iStatBase;
     *   local iGreen = (iStatModded > iStatBase); "<table bgcolor=black>
     *   <tr><td><font color=\"<<fontColor>>\">[<<fullStatNames[statistic]>>
     *   check... "; if(iGreen){ "</font><font
     *   color=#00FF00><<iStatModded>></font><font color=\"<<fontColor>>\"> vs.
     *   d20.... result: <<iRandy>>"; }else{ "<<iStatModded>> vs. d20....
     *   result: <<iRandy>>"; } if(iRandy > iStatModded){ "
     *   FAILURE!]</font></td></tr></table><.p>"; return nil; } "
     *   SUCCESS!]</font></td></tr></table><.p>"; return true; }
     */
  statBonus = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]// 18 of them bonus to hit, etc.
  //spellEffects = [] // spell objects (both good and bad, buffs and curses) go in this list and stats are modified on the fly based on statbuffs
      
  getDamageComplex(attacker,target){
     if(self.alwaysHit!=nil) return self.getDamage();// alwaysHit ignores resistance checks
    //stats = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] // 18 of them
    //statNames = ['str','int','dex','agi','wis','cha','con','sta','nature','death','fire','cold','magic','poison','disease','divine','wil','end']
     local iChance = attacker.stats[3];// dexterity check
     // makes more sense do do a d20 check vs. stat on a "to hit"

        // *** BOOKMARK: Maybe use the next line??      
        // local iPass = attacker.twentyDieRoll(attacker.stats[2],nil);// checks with modded stats (i.e. clothing items had a bonus? then add it)
     // if we hit then calculate monster resistance number to subtract damage        
        
        
     // old code below   
     if((rand(100)+1) <= iChance){
         // hit!
         //say(self.hitMsg);
         //"**** HIT!!! ";
         return self.getDamage();
     }else{
         //say(self.missMsg);
         // miss!
         //"*** MISS!!! ";
         return 0;
     }
  }
  
  // unlike player stats, weapon DMG uses stats 9-15 for TYPE of damage (nature, death, fire, etc.  
  // we will just inherit these values below from CromexxHuman class
    
  //stats = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] // 18 of them
  //statNames = ['str','int','dex','agi','wis','cha','con','sta','nature','death','fire','cold','magic','poison','disease','divine','wil','end']
  //fullStatNames = ['Strength','Intelligence','Dexterity',
  //                   'Agility','Wisdom','Charisma','Constitution',
  //                   'Stamina','Nature','Death','Fire','Cold',
  //                   'Magic','Poison','Disease','Divine','Willpower','Endurance']
  //statBonus = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]// 18 of them bonus to hit, etc.
  
  hitSuccessNumber = 5 // hitMod + weilder DEX - target AGI is this number or greater then a hit was made
  hitModifier = 0 // override - this is the plus amount to add to the chance of a hit
  calculateHitMiss(weilder,target){
     if(target==nil) return 0;// miss!
     if(weilder==nil) return 0;// miss!
     // basic formula is weapon hit modifier + weilder's dexterity - target's agility (to avoid)
     local iChance = self.hitModifier + rand(weilder.stats[2]) - rand(target.stats[3]);
     if(self.alwaysHit!=nil) iChance=999;
     if(iChance >= self.hitSuccessNumber){
        // the monster hits automatically based on naturally higher stats than target
        return true; // hit!
     }else{
        return nil; // miss!
     }     
  }
  getDamage(){
     return 0; // objects and classes must override
  }
  iobjFor(AttackWith){
    verify(){ logicalRank(110,'likely'); }
    check(){}
    action(){}
  }
;

modify Person
    calculateReward(attacker){     
        return self.xpReward; // each class can modify
    }
    xpReward = 0 // overrride to increase experience point reward
    hitpoints = 20 // override to increase or decrease as needed

   notifyXPReward(points,monster){
    self.xp+=points;// should modify formula based on monster level
    self.xpBar+=points;
    local iLevelup = gameMain.levelXP[self.level];// grab the total XP required to advance to next level
    if(xpBar>=iLevelup){
       "{The self/him} levels up! ";
       self.level++;
       if(self==gPlayerChar) "Welcome to level <<self.level>> ! ";
       local iDiff=self.xpBar-iLevelup;// grab the difference
       if(iDiff<=0) iDiff=0;
       self.xpBar=iDiff;// reset the xp bar and add the difference (if any) towards the next level
       // increase manapool and hp pool a little
       local manaOld = self.basemanapoints;
       local hpOld = self.baseHitpoints;
       local manaNew = (manaOld + (rand(12) + (self.manaMod)));     
       local hpNew = (hpOld + (rand(12) + (self.conMod)));
       if(self.characterClass != nil){
         if(self.characterClass == 'wizard'){
            manaNew = manaNew + (self.level * self.stats[2]);// intelligence  use bignumber.getFloor() to round down <= this number
         }
       }else{
           "<br><table bgcolor=black><tr><td><font color=yellow>{You/he} get{s} <<points>> points of experience!</font></td></tr></table> ";
       }
       self.basemanapoints = manaNew;
       self.baseHitpoints = hpNew;
    }
  }
  /*
   *  This is an internal method only. Do not call this directly.
   *  Various monster classes may override this to change defenses 
   *  based on what attacks they perceive (through this notification).
   *
   *  The player will most likely receive the default attackString showing 
   *  a missed attack.
   *  
   *  iAttackType is an integer from 0 to 11.
   *     0 = unknown
   *     1 = melee (includes: bite, claw, punch, kick)
   *     2 = blunt
   *     3 = piercing (includes: spears, javelins)
   *     4 = stabbing (includes: swords, daggers)
   *     5 = slicing (includes: axes, halberds, swords, daggers, etc.)
   *     6 = nature magic
   *     7 = death magic
   *     8 = fire magic
   *     9 = cold magic
   *    10 = general magical attack
   *    11 = poison
   *
   */
  receiveMissedAttackNofication(attacker,iAttackType,attackString){
        if(iAttackType == nil) iAttackType = 0;
        if(self == gPlayerChar) say(attackString);
  }

  /* 
   * This (self) creature or person TAKES damage from attacker and shows it damageString
   *
   * NOTE: Do not call this method directly. It is called from dealDamage method as an internal
   *       notifier from attacker to victim.
   */   
  takeDamage(damage,attacker,damageString){
         self.hitpoints -= damage;
         local s0 = damageString;
         if(self.hitpoints <=0){
              if(self == gPlayerChar){
                    // player death
                    gMessageParams(attacker);
                    s0+= '<br>You have been slain by {subj attacker}{the attacker/him}! ';
                    say(s0);
                    finishGameMsg(ftDeath, [finishOptionUndo,finishOptionFullScore,finishOptionRestart]); 
                    exit;
              }
              self.hitpoints = 0;
              s0 += '<br>\^' + self.name + ' dies! Its corpse evaporates in an orange cloud of smoke. ';
              say(s0);
              if(self.xpReward>0){              
                   attacker.notifyXPReward(self.calculateReward(attacker),self);
              }
              self.notifyFaction(attacker);
              self.moveInto(nil);   
         }else{
              say(s0);     
         }         
  }
  /*
   * This (self) creature or person DEALS damage to a target, showing damageString + generic damage message
   *
   * iDamageType is an integer from 0 to 11.
   *     0 = unknown
   *     1 = melee (includes: bite, claw, punch, kick)
   *     2 = blunt
   *     3 = piercing (includes: spears, javelins)
   *     4 = stabbing (includes: swords, daggers)
   *     5 = slicing (includes: axes, halberds, swords, daggers, etc.)
   *     6 = nature magic
   *     7 = death magic
   *     8 = fire magic
   *     9 = cold magic
   *    10 = general magical attack
   *    11 = poison
   *
   */
  dealDamage(damage,iDamageType,target,damageString){
       local s0 = damageString;
       if(damage==nil) damage=0;
       if(damage==0) exit; // do nothing if no damage to apply
       self.lastAttacker=target;
       
       damageString += '<table><tr><td bgcolor=black>'; 
       local sName = (target == gPlayerChar) ? 'You' : target.name;
       s0 += '<font color=#00FFFF>...\^' + sName + ' take';
       s0 += (target != gPlayerChar) ? 's' : ' ';
       s0 += ' ' + toString(damage) + ' point';
       s0 += (damage > 1) ? 's damage' : ' damage';
       s0 += '!</font></td></tr></table><.p>';      

       // is the attacker who attacked us in our enemy list? if not, add them!
       if(self.enemyList!=nil){
          local iMatch = nil;
          foreach(local oEnemy in self.enemyList){
             if(oEnemy==target) iMatch=true;
          }
          if(iMatch==nil) self.enemyList+=target;
       }else{
          self.enemyList+=target;
       }
       // notify target so it can take damage
       target.takeDamage(damage,self,s0);
  }
  kickAttack(target){ } // override these as necessary - this is where the monster decides to attack "target" in a certain way
  punchAttack(target){ } // override
  biteAttack(target){ } // override
  clawAttack(target){ }
  breathAttack(target){ }
  spellAttack(target){ } // spell cast from spells located in self.spellList[] 
  sayAttackMsg = true  // or nil to NOT say the attackMsg on first attack
  attackMsg = 'By the by, I left my heart in Timbucktoo! Aaaaaa-taaaaack! '
  saidAttackMsg = nil // or true if we already said it
  weildedAttack(target,mainweap,offhand){ 
        // attack target with main weapon of mainweap, and offhand weapon if self can dual wield.
        if(mainweap==nil) exit;// if we have no primary weapon then exit
        if(target==nil) exit;// if no target then nothing to attack, so exit
        local s0 = '';
        // say the initial attack message?
        if((target == gPlayerChar) && (self.sayAttackMsg==true) && (saidAttackMsg == nil)){
            s0 += '<font color=#00FFFF>' + self.attackMsg + '</font><br>';
            self.saidAttackMsg = true;
        }
        s0+='<font color=#00FFFF>\^' + self.name + ' ';
        // add in a random attack word 'stabs','slashes', 'slices', 'swings', etc.
        s0 += mainweap.attackWords[rand(mainweap.attackWords.length)+1];
        s0 += '...</font> ';   
        s0 += '<font color=#00FFFF>...';
        local iDmg = mainweap.getDamageComplex(self,target);
        if(
           ( iDmg == 0 ) || 
           ( mainweap.calculateHitMiss(self,target)==nil ) 
          ){
              s0+='but misses!</font><.p> ';
              target.receiveMissedAttackNofication(self,3,s0);
        }else{              
              local sName = (target == gPlayerChar) ? 'YOU' : target.name;
              s0+=' and hits \^' + sName + ' for ';
              s0=s0 + toString(iDmg) + ' damage!</font><.p>';      
              self.dealDamage(iDmg,3,target,s0);// ... which then calls target.takeDamage method
        }
  }
 ;

// This adds the verb 'smash' to the list of attack types
modify VerbRule(Attack) 
    ('attack' | 'kill' | 'hit' | 'kick' | 'punch' | 'smash') singleDobj
    : 
; 
// ... note that we did not need to modify the Action area of the above
// ... so all after the : is left intact (see: en_us.t)

// this adds the verb 'smash' to the list of attack types
modify VerbRule(AttackWith)
    ('attack' | 'kill' | 'hit' | 'kick' | 'punch' | 'strike' | 'smash')
        singleDobj
        'with' singleIobj
    : 
;

…Note that this gets even more complicated than the above as I’m also slowly developing a magic combat system as well. I hope to, before the end of the year, submit something to one of the IF-competitions to demonstrate all this in a game. I’ll submit full source code to it as well so others can re-use it as they like. For now, hopefully this gives you an idea of what to do and how complicated it can get making a full RPG style combat system.

  • Bill

EDITED: Forgot to put the methods in gameMain that some of the above code refers to, so here you go (plus other goodies like money handling. It’s for an RPG after all, right?)



// put these includes at the TOP of your beginning game code (before gameMain, etc.)
#charset "us-ascii"

#include <adv3.h>
#include <en_us.h>

#ifndef _BIGNUM_H_
#include <bignum.h>
#endif

// ... then just put the rest of the stuff below in your:  gameMain: GameMainDef

    // call this to see if we can display images
    showImages(){
      if(systemInfo(SysInfoInterpClass) != SysInfoIClassHTML) return nil;// no graphics HTML support for the user's interpreter
      if((systemInfo(SysInfoPrefImages)) != 1) return nil;// user specifically set "no images" checkbox
      return true;
    }

    /* character level table
     *    Levels 1-70
     *    These are the XP numbers required to reach the next character level.
     *    NOTE: These level numbers are based off World of Warcraft XP level data.
     */
     //          1   2    3   4    5    6    7    8    9    10
    levelXP = [400,900,1400,2100,2800,3600,4500,5400,6500,7600,
               8800,10100,11400,12900,14400,16000,17700,19400,21300,23200,
               25200,27300,29400,31700,34000,36400,38900,41400,44300,47400,
               50800,54500,58600,62800,67100,71600,76100,80800,85700,90700,
               98500,10100,106300,111800,117500,123200,129100,135100,141200,147500,
               153900,160400,167100,173900,180800,187900,195000,202300,209800,494000,
               574700,614400,650300,682300,710200,734100,753700,768900,779700]

     myBank = new BigNumber('0') // BigNumber('99123456') // the "master" bank variable for the player
     myCarriedMoney = new BigNumber('50') // 12345678') // '50') // temp variable mostly - recalculated "on the fly" based on carried gold, silver and copper
     myCarriedGold = new BigNumber('2550')    // actual
     myCarriedSilver = new BigNumber('320')  // actual
     myCarriedCopper = new BigNumber('50') // actual

getPlayerBank(){
           // local gold = new BigNumber(myBank.getAbs() / 10000).getFloor();// floor is biggest int less than this number. i.e. 12.3456 = 12
           local gold = new BigNumber(gameMain.myBank.getAbs() / 10000).getFloor();
           local silver = new BigNumber(gameMain.myBank.getAbs() / 100).getFloor();
           silver = silver.divideBy(100)[2];// second list item is remainder
           local copper = new BigNumber(gameMain.myBank.getAbs()).getFloor();
           copper = copper.divideBy(100)[2];// second list item is remainder
           local s0 = 'In the bank you have: ' + 
                      gold + 'g ' + 
                      silver + 's ' + 
                      copper + 'c ' + '';
                      // '\bTotal (converting all to copper): ' + gameMain.myBank + 'c ';
           return s0;
    }

    getPlayerCarriedMoney(){
           //       --... // local gold = new BigNumber(myBank.getAbs() / 10000).getFloor();// floor is biggest int less than this number. i.e. 12.3456 = 12
           // local gold = new BigNumber(gameMain.myCarriedMoney.getAbs() / 10000).getFloor();
           // local silver = new BigNumber(gameMain.myCarriedMoney.getAbs() / 100).getFloor();
           // silver = silver.divideBy(100)[2];// second list item is remainder
           // local copper = new BigNumber(gameMain.myCarriedMoney.getAbs()).getFloor();
           // copper = copper.divideBy(100)[2];// second list item is remainder
           local gold = 0;
           if(gameMain.myCarriedGold > 0) gold = new BigNumber(gameMain.myCarriedGold.getAbs()).getFloor();
           local silver = 0;
           if(gameMain.myCarriedSilver > 0) silver = new BigNumber(gameMain.myCarriedSilver.getAbs()).getFloor();
           local copper = 0;
           if(gameMain.myCarriedCopper > 0) copper = new BigNumber(gameMain.myCarriedCopper.getAbs()).getFloor();           
           local s0 = 'Your carried money is: ' + 
                      gold + 'g ' + 
                      silver + 's ' + 
                      copper + 'c';
           return s0;
    }    

    // call this to see if we can display images
    showImages(){
      if(systemInfo(SysInfoInterpClass) != SysInfoIClassHTML) return nil;// no graphics HTML support for the user's interpreter
      if((systemInfo(SysInfoPrefImages)) != 1) return nil;// user specifically set "no images" checkbox
      return true;
    }

    allowNoises = true  // NOTE: should prompt the player for this at start of game

    // where mysound is a '' string of a sound file
    // where soundlayer is either: 'foreground','background','ambient', etc.
    // where interruptYN is true or nil
    playSound(mysound,soundlayer,interruptYN) {
        if(mysound && gameMain.playSounds()){
            if(interruptYN){
                "<SOUND SRC=\"web/<<mysound>>\" layer=foreground interrupt>";    
            }else{
                 "<SOUND SRC=\"web/<<mysound>>\" layer=foreground>";    
            }
        }
    }

    //    "<img src=\"startRoom2.png\"> <.p>";        
    displayPic(mypic){
        if(mypic && gameMain.showImages()){
            "<img src=\"web/img/<<mypic>>\">";
        }
    }

    displayMonsterPic(mypic){
        if(gameMain.showImages()){
            "<img src=\"web/img/monsters/<<mypic>>\"><.p><br>";
        }
    }

    displayRoomPic(mypic){
        if(mypic && gameMain.showImages()){
            "<img src=\"web/img/rooms/<<mypic>>\"><.p><br>";
        }        
    }

     playSounds(){
          if(gameMain.allowNoises != true) return nil;
          if(systemInfo(SysInfoWav)) return true;   
          return nil;
     }

    getYesNo(){
        local s0 = '';
        local iDone = 0;
        do{
                           "<br> (y/n) > ";
                           s0 = inputManager.getInputLine(nil, nil);
                           s0 = s0.toLower();
                           if(s0 == 'yes') s0 = 'y';
                           if(s0 == 'no') s0 = 'n';
                           if((s0 == 'y') || (s0 == 'n')){
                              iDone++;
                           }else{
                              "Please enter yes or no.<br>";
                           } 
                           "\n";
        }while(iDone==0);
        if(s0 == 'y')
            return true;
        return nil;
    }

NOTE: For the money routines to work you’ll need to include “bignum.h” in your project.

I know this is more than you asked for but if you are interested in combat, chances are you’re interested in an RPG money system as well, so that’s why I’m also including it for you (to save time researching how to do this). Below are some more routines (used by a banker NPC):

  showBank(){
     local sa = gameMain.getPlayerBank();
     local sb = gameMain.getPlayerCarriedMoney();     
     "<table border=1 width=350 bgcolor=#ffffff>
        <tr><td><font color=#000000>\b<<sa>></font></td></tr>
        <tr><td><font color=#000000>\b<<sb>></font></td></tr>
        <tr><td><font color=#000000>\b(1) Deposit\b(2) Withraw\b(3) Goodbye</font></td></tr>
      </table>";
    local iDone = 0;
    local s0 = '';
    do{
              "<br> >";
              s0 = inputManager.getInputLine(nil, nil);
              s0 = s0.toLower();
              if((s0 == 'deposit') || 
                (s0 == 'withdraw') ||
                (s0 == 'goodbye') ||
                (s0 == 'bye') ||
                (s0 == '1') ||
                (s0 == '2') ||
                (s0 == '3')){ 
                   iDone++;
              }else{
                "Please select <b>1</b> - <b>3</b><br>";
              } 
              "\n";
    }while(iDone==0);
    if((s0=='3') || (s0=='goodbye') || (s0 =='bye')){
             if(bankerEddy.agendaList != nil){
                 foreach(local obj in bankerEddy.agendaList){
                     bankerEddy.removeFromAgenda(obj);// clear the agenda list
                 }
             }
             bankerEddy.curState.endConversation(gPlayerChar,endConvBye);// endConvBye);// see: endConvxxx in adv3.h - see: actor.t             
    }else if((s0=='2') || (s0=='withdraw')){
           bankerEddy.withdraw();       
    }else{
           bankerEddy.deposit();
    }
  }

  deposit(){
           local gold = new BigNumber(gameMain.myCarriedGold.getAbs()).getFloor();
           local silver = new BigNumber(gameMain.myCarriedSilver.getAbs()).getFloor();
           local copper = new BigNumber(gameMain.myCarriedCopper.getAbs()).getFloor();           
           if(gold + silver + copper == 0){
               "You have no money in your posession to deposit!\b";
               inputManager.pauseForMore(true);  
               bankerEddy.showBank();              
           }else{
               if(copper != 0){
                     bankerEddy.depositAmount(copper,'copper');
               }
               if(silver != 0){
                     bankerEddy.depositAmount(silver,'silver');
               }               
               if(gold != 0){
                     bankerEddy.depositAmount(gold,'gold');
               }
               inputManager.pauseForMore(true);  
               bankerEddy.showBank();              
           }
  }

  /*
   *   where oMoneyObject = gameMain.myCarriedGold, ...silver, ... copper
   *                sName = 'gold', 'silver' or 'copper'
   */
  depositAmount(oMoneyObject,sName){
          oMoneyObject = new BigNumber(oMoneyObject.getAbs()).getFloor();
          local iDone = 0;
          local s0 = '';
          local oBigNumborf;
          local iFailDigitTest = 0;
          local digitPat = new RexPattern('<digit>');// matches a single digit in a string
          do{
              "<br>Please enter amount of <<sName>> to deposit >";
              s0 = inputManager.getInputLine(nil, nil);
              if(s0 == nil) s0 = '0';
              if(s0 == '') s0 = '0';
              s0 = s0.toLower();
              // rex search for <digit> ... this stops cheaters from trying to trick the parser with math tokens, etc. 
              iFailDigitTest = 0;// reset flag 
              for(local i = 0; i <= s0.length;i++){
                if(! rexMatch(digitPat,s0,i)) iFailDigitTest++;
              }            
              oBigNumborf = new BigNumber(s0);
              // getFraction() returns everything to the right of the decimal point, if this is > 0 it is a fraction
              if(oBigNumborf.getFraction() > 0){
                 "Please enter whole numbers.<br>";
              }else if(iFailDigitTest > 0){
                  "Please enter a whole number without any alphabetical or special characters (including the avoidance of decimal points). ";
              }else if(toInteger(s0) > oMoneyObject){
                 "That\'s more <<sName>> than you have available.<br>";
              }else if(toInteger(s0) < 0){
                 "You can\'t deposit negative amounts of <<sName>>.<br>";
              }else if(toInteger(s0) >= 0){
                 "The banker says,\"Depositing <<s0>> <<sName>> to your account.\"<br>";                 
                 if(sName == 'copper'){
                    gameMain.myBank += toInteger(s0);// copper gets directly added
                    gameMain.myCarriedCopper -= toInteger(s0);        
                 }else if(sName == 'silver'){
                    gameMain.myBank += (toInteger(s0) * 100); // silver converted to copper is * 100
                    gameMain.myCarriedSilver -= toInteger(s0);
                 }else if(sName == 'gold'){
                    gameMain.myBank += (toInteger(s0) * 10000);// gold converted to copper is * 10000
                    gameMain.myCarriedGold -= toInteger(s0);
                 }
                 iDone++;
              }else{
                 "You must enter a numeric amount from 0 to <<toInteger(s0)>>.<br>";
              }
              "\n";
          }while(iDone==0);
  }

  withdraw(){
          local gold = new BigNumber(gameMain.myBank.getAbs() / 10000).getFloor();
          local silver = new BigNumber(gameMain.myBank.getAbs() / 100).getFloor();
          silver = silver.divideBy(100)[2];// second list item is remainder
          local copper = new BigNumber(gameMain.myBank.getAbs()).getFloor();
          copper = copper.divideBy(100)[2];// second list item is remainder
          if(gold + silver + copper == 0){
               "You have no money in the bank to withdraw!\b";
               inputManager.pauseForMore(true);  
               bankerEddy.showBank();              
           }else{
               if(copper != 0){
                     bankerEddy.withdrawAmount(copper,'copper');
               }
               if(silver != 0){
                     bankerEddy.withdrawAmount(silver,'silver');
               }               
               if(gold != 0){
                     bankerEddy.withdrawAmount(gold,'gold');
               }
               inputManager.pauseForMore(true);  
               bankerEddy.showBank();              
           }  
  } 

  /*
   *   where oMoneyObject = gameMain.myCarriedGold, ...silver, ... copper
   *                sName = 'gold', 'silver' or 'copper'
   */
  withdrawAmount(oMoneyObject,sName){
          local iDone = 0;
          local s0 = '';
          local oBigNum;
          local iFailDigitTest = 0;
          local digitPat = new RexPattern('<digit>');// matches a single digit in a string
          do{
              "<br>Please enter amount of <<sName>> to withdraw >";
              s0 = inputManager.getInputLine(nil, nil);
              if(s0 == nil) s0 = '0';
              if(s0 == '') s0 = '0';
              s0 = s0.toLower();
              // rex search for <digit> ... this stops cheaters from trying to trick the parser with math tokens, etc. 
              iFailDigitTest = 0;// reset flag 
              for(local i = 0; i <= s0.length;i++){
                if(! rexMatch(digitPat,s0,i)) iFailDigitTest++;
              }
              oBigNum = new BigNumber(s0);
              // getFraction() returns everything to the right of the decimal point, if this is > 0 it is a fraction
              if(oBigNum.getFraction() > 0){
                  "Please enter whole numbers.<br>";
              }else if(iFailDigitTest > 0){
                  "Please enter a whole number without any alphabetical or special characters (including the avoidance of decimal points). ";
              }else if(toInteger(s0) > oMoneyObject){
                 "That\'s more <<sName>> than you have in the bank.<br>";
              }else if(toInteger(s0) < 0){
                 "You can\'t withdraw negative amounts of <<sName>>.<br>";
              }else if(toInteger(s0) >= 0){
                 "The banker says,\"Withdrawing <<s0>> <<sName>> from your account.\"<br>";                 
                 if(sName == 'copper'){
                    gameMain.myBank -= toInteger(s0);
                    gameMain.myCarriedCopper += toInteger(s0);        
                 }else if(sName == 'silver'){
                    gameMain.myBank -= (toInteger(s0) * 100); // silver converted to copper is * 100
                    gameMain.myCarriedSilver += toInteger(s0);
                 }else if(sName == 'gold'){
                    gameMain.myBank -= (toInteger(s0) * 10000);// gold converted to copper is * 10000
                    gameMain.myCarriedGold += toInteger(s0);
                 }
                 iDone++;
              }else{
                 "You must enter a numeric amount from 0 to <<toInteger(s0)>>.<br>";
              }
              "\n";
          }while(iDone==0);
  }

Oh yeah, one other class I forgot to add above is the FactionGroup class:

class FactionGroup: object
  name = 'fiends of frost' // override
  playerStanding = 50 // 0-75 is bad,76-125 is dubious, etc.,200+ is favored 
  takeFactionHit(factionAdjustObject,killer){
      if(killer!=gPlayerChar) exit;
      if(factionAdjustObject.factionHit==nil) exit;
      self.factionAdjust(factionAdjustObject.factionHit, gPlayerChar);
  }
  verifyAggro(spottedactor){
      if(gPlayerChar==spottedactor){
         if(self.playerStanding < 0) return true;// below zero is very bad aggro
      }
      return nil;
  }
  getFactionStandingMsg(){
      local s0 = self.name + ': ';
      local i = playerStanding;
      if(i == -250){ s0 += 'sworn enemy';
      }else if(i < -200){ s0 += 'hated';
      }else if(i < -100){ s0 += 'despised';
      }else if(i < 0){ s0 += 'loathed';
      }else if(i < 49){ s0 += 'distrusted';
      }else if(i < 99){ s0 += 'friendly';
      }else{ s0 +='ally';
      }// end if
      return s0;
  }
  factionAdjust(iAmount,oFactionReceiver){
      if(oFactionReceiver == nil) exit; // do nothing if not called correctly
      if(oFactionReceiver != gPlayerChar) exit; // only track if player is involved
      self.playerStanding += iAmount;
      if(self.playerStanding < -250) self.playerStanding = -250;
      if(self.playerStanding > 250) self.playerStanding = 250;
      local s0 = '<br>';
      s0 += '<table><tr><td bgcolor=black>';
      if(iAmount < 0){
          s0 += '<font color=#99FFFF>';// lightblue for faction hit
      }else{
          s0 += '<font color=yellow>';// yellow font for faction gain
      }
      s0 += 'Your faction standing with ' + self.name + 
            ' just got ' + ((iAmount < 0) ? 'worse' : 'better');
      s0 += '.</font>';
      if((self.playerStanding == -250) && (iAmount < 0)){
          s0 = '<br><font color=#99FFFF>Your faction standing with ' + self.name + 
               ' could not possibly get any worse!</font>';
      }
      if((self.playerStanding == 250) && (iAmount > 0)){
          s0 = '<br><font color=#99FFFF>Your faction standing with ' + self.name + 
               ' could not possibly get any better!</font>';
      }
      s0 += '</td></tr></table>';
      say(s0);
  }
;

…That should be about it… and as much code as that all is, it’s still a work in progress. You can add any or all of this to your Tads 3 game(s) as you like. So far it works but it’s limited to monsters attacking only with simple Weapon class weapons. In my Beta game the player also gets to learn a few magic spells for combat, one of which is area of effect, the other is a direct damage “nuke” spell.

Woe, that is much more powerful than Breslin’s system (assuming t2combat and t3combat work the same way). Looking forward to your game’s release.

Thanks. Yeah, that’s for Tads 3. In this system I set things up so it’s possible for monsters to level up off killing non-player characters (or even the player). So for example, if I sent a wave of NPC guards after a monster and the monster killed them all, the monster is actually getting XP off all those kills and can level up, getting more mana and hitpoints each level. I don’t know of any other RPG combat system (in existing computer games) that does this but if you think about it, the monsters should be able to level up as well and get tougher if they’re not defeated. Probably lower level monsters, this wouldn’t be much of an issue but for maybe things as tough as dragons, yep, you’d have to have a pretty tough group to fight some of the high level dragons in the game and if you don’t win after a number of attempts you could expect that dragon to level up off your team’s failures.

As far as monsters leveling, I think Kerkerkruip allows that sort of thing. Rather interesting. I’ve never seen that in any other text-based RPG’s. Man, your game is probably gonna be epic. (P.S., contact me if you need a beta testor. It’s been a while since I’ve played a good RPG-type game.)

That’s extensive :slight_smile: For my RPG I’m going to tackle coding the combat too. I find that’s the best way. Rather than big texts I didn’t write I prefer to know exactly what’s happening in the code and where.