intfiction.org

The Interactive Fiction Community Forum
It is currently Thu May 23, 2013 9:04 am

All times are UTC - 6 hours [ DST ]




Post new topic Reply to topic  [ 10 posts ] 
Author Message
PostPosted: Sat Apr 30, 2011 7:39 pm 
Offline

Joined: Sun Jan 30, 2011 10:20 pm
Posts: 32
My questions are rather to the point, but I guess I’d better explain where I’m going with all this:

The ultimate goal here is to create a system of random encounters similar to a basic RPG video game. That is, when wandering into a certain area, there should be a random chance of certain monsters being moved into the area and attacking the player until they are killed. I have… quite a load of stuff to figure out before I can get all the mechanics down and this is mostly proof of concept just to see If I can do it, but for the moment I at least succeeded in creating a killable monster to attack my character. This is the map and player character:

Rant: show
Code:
startRoom: Room 'Starting Room'
    "This is the start room. "
    west = activeBattleRoom
    east = randomEncounterRoom
;
 
activeBattleRoom: Room 'Active Battle Room'
    "This is the active battle room. "
    east = startRoom
;

randomEncounterRoom: Room 'Random Encounter Room'
    "This is the testing area for random encounters. "
    west = startRoom
;

me: Actor
    location = startRoom
;
+myHP: Component 'my hp/health/life/point*points' 'your HP'
    desc = "Current Health: <<CurrentHP>>"
    CurrentHP = 2000
    LoseHP(Damage)
    {
        CurrentHP -= Damage;
        if (CurrentHP <= 0)
            finishGameMsg('You lose!', [finishOptionUndo]);
    }
;
+myATK: Component 'my attack/point*points'
    desc = "ATK: <<CurrentATK>>"
    CurrentATK = 100
;


Slime here :
Rant: show
Code:
slime: UntakeableActor 'blue slime' 'Slime' @activeBattleRoom
    "It looks like a small gelatinous puddle of blue sludge with eyes. "
    HP = 1000
    MaxHP = 1000
    ATK = 100
    LoseHP(Damage)
    {
        HP -= Damage;
    }   
    HealHP(Amount)
    {
        HP += Amount;
        HP = min(HP, MaxHP);
    }
    dobjFor(Attack)
    {
        action()
        {
            LoseHP(myATK.CurrentATK);
            "You strike the Slime. ";
            if (HP <= 0)
            {
                moveInto(nil);         
                "You defeated the Slime. ";
                HP = MaxHP;       
            }
            else
                "The Slime has <<HP>> HP left. ";
        }
    }
;

+slimeAttackAgenda: AgendaItem
    isReady = (inherited() && me.location == slime.location)
    initiallyActive = true
    invokeItem()
    {
        "The Slime charges at you! ";
        myHP.LoseHP(slime.ATK);
    }
;

just sits on its ass until I walk into its room and it attacks me. If it wins, I lose, if I win, Slime is moved to nil and its HP is returned to full (1000). The idea is to, coding-wise, only have a single of each monster that will be moved to nil upon defeat, healed completely and randomly have a chance of being moved back into a certain area whenever I step into it.

This approach… may very likely not be optimal, and I need to work out a way to prevent the player from leaving an area before killing the monster (stop him from fleeing, basically), but there are other issues to work out before then so I’ll ignore that and a slew of other problems I’m thinking of for now.

Right now, my first order of business is to figure out how to trigger a method when I step the into randomEcnounterRoom area (some form of EventList perhaps?). I created a clone of Slime for this:
Code:
redSlime: UntakeableActor 'red slime' 'Red Slime' @nil
    "It looks like a small gelatinous puddle of red sludge with eyes. "
    HP = 1000
    MaxHP = 1000
    ATK = 100
    LoseHP(Damage)
    {
        HP -= Damage;
    }   
    HealHP(Amount)
    {
        HP += Amount;
        HP = min(HP, MaxHP);
    }
    dobjFor(Attack)
    {
        action()
        {
            LoseHP(myATK.CurrentATK);
            "You strike the Red Slime. ";
            if (HP <= 0)
            {
                moveInto(nil);               
                "You defeated the Red Slime. ";
                HP = MaxHP;
            }
            else
                "The Red Slime has <<HP>> HP left. ";
        }
    }
;

+redSlimeAttackAgenda: AgendaItem
    isReady = (inherited() && me.location == redSlime.location)
    initiallyActive = true
    invokeItem()
    {
        "The Red Slime charges at you! ";
        myHP.LoseHP(redSlime.ATK);
    }
;


So I need to find a way to call redSlime.moveInto(randomEncounterRoom) when I step into the room.

With that done, I’d need to figure out how to make it so that this method is called sometimes and not others, thus making Red Slime’s appearance a random occurrence. And I’d need to add a check to make sure that the method isn’t called if (me.location == redSlime.location) return true, so that the game doesn’t keep trying to summon Red Slime when it’s already fighting me.


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 1:07 am 
Offline

Joined: Tue Apr 27, 2010 1:02 pm
Posts: 797
Something like this should work.

Code:
modify randomEncounterRoom
   enteringRoom(traveler)
   {
      inherited(traveler);
      
      // yields a number between 0 and 99
      local roll = rand(100);

      // % chance of random encounter
      local freq = 20;

      if (roll < freq && !redSlime.getOutermostRoom() == this)
         redSlime.moveInto(this);
   }

   beforeTravel(traveler, connector)
   {
      if (redSlime.getOutermostRoom() == this)
      {
         failCheck('You cannot run from the RED SLIME! ');
      }
      
      inherited(traveler, connector);
   }
;


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 7:14 am 
Offline
User avatar

Joined: Sun Feb 03, 2008 5:55 am
Posts: 298
Bcressey's enteringRoom method is fine for making the monster appear, but personally, I'd try and keep as much of the code regarding the monster's behaviour as possible in a parent class. That way, you can be certain that things will happen the way you expect whenever the player is in the same room as a monster - and you don't have to worry about what happens if you put a monster in a room that doesn't normally have combat, or if the player enters the arena when there are no monsters there.

The Actor class has its own beforeTravel method, which fires whenever travel is about to occur in the presence of that actor (or, at least, full on travel, as opposed to teleporting things around with moveIntoForTravel). So now we just check that the person travelling is actually the player (or anyone else the monster might want to interfere with) and call exit or failCheck if we want to stop them.

Code:
class Monster: Person
    beforeTravel(traveller, connector)
    {
        if(traveller==gPlayerChar)
        {
            failCheck(blockTravelMsg);
        }
        else
            inherited(traveller, connector); //I always forget this bit and bad things always happen
    }
    blockTravelMsg='\^'+theName+' blocks your path!'
;

theName outputs the name of the monster, plus the word "the" if necessary, and since the "the" (if it appears) will be lower case, we add \^ to the start to make the first letter upper case.

Implementing the class, you'd have something like this:

Code:
blah: Monster 'blah' 'blah'
    blockTravelMsg='The blah works its apathetic magic, and you feel too lethargic to leave.'
;


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 2:28 pm 
Offline

Joined: Sun Jan 30, 2011 10:20 pm
Posts: 32
Sorry to be a bother, but I can’t seem to get this to work. Just putting the code in as it is doesn’t work and I’ve already tried putting the enteringRoom(traveller) method in the actual randomEncounterRoom code (Why use Modify in this case btw?) and increasing freq to 99 to ensure I’m not just getting bad rolls. Nothing shows up no matter how long I exit and re-enter. Also the compiler gives me warning regarding the “this” in “!redSlime.getOutermostRoom() == this” and “redSlime.moveInto(this);” which I’m not sure of what they are in the first place. Are they placeholders that I’m meant to replace with the room names? I tried that and the warnings go away, but the Slime still doesn’t show up.

Code:
startRoom: Room 'Starting Room'
    "This is the start room. "
    north = battleRoom
    west = activeBattleRoom
    east = randomEncounterRoom
;

battleRoom: Room 'Battle Room'
    "This is the battle room. "
    south = startRoom
;
   
activeBattleRoom: Room 'Active Battle Room'
    "This is the active battle room. "
    east = startRoom
;

randomEncounterRoom: Room 'Random Encounter Room'
    "This is the testing area for random encounters. "
    west = startRoom
;

modify randomEncounterRoom
    enteringRoom(traveler)
    {
        inherited(traveler);
        local roll = rand(100);
        local freq = 20;
        if (roll < freq && !redSlime.getOutermostRoom() == this)
            redSlime.moveInto(this);
    }
;


As for the Monster class thing, yes I’m aware of that. Once I got most of the individual mechanics down, I was planning on cleaning up the code by nesting all the generic monster behaviour in a custom actor class. Thanks for the theName part, that solves another problem I was thinking of.


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 2:50 pm 
Offline
User avatar

Joined: Sun Feb 03, 2008 5:55 am
Posts: 298
Arag-e wrote:
Also the compiler gives me warning regarding the “this” in “!redSlime.getOutermostRoom() == this” and “redSlime.moveInto(this);” which I’m not sure of what they are in the first place. Are they placeholders that I’m meant to replace with the room names? I tried that and the warnings go away, but the Slime still doesn’t show up.

I think Ben meant to use "self" instead of "this", which basically is a short-hand way to refer to the object that contains the function.

Try changing the condition to:

Code:
if (roll < freq && redSlime.getOutermostRoom() != self)

I'm not entirely sure why the original condition didn't work, but:

Code:
if (roll < freq && !(redSlime.getOutermostRoom() == self))

is also fine. My guess is that the not operator is applying to the getOutermostRoom() for some reason, and, uh... No, I don't get it. :?


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 4:49 pm 
Offline

Joined: Sun Jan 30, 2011 10:20 pm
Posts: 32
Pacian wrote:
Code:
if (roll < freq && !(redSlime.getOutermostRoom() == self))

is also fine. My guess is that the not operator is applying to the getOutermostRoom() for some reason, and, uh... No, I don't get it. :?


That did it! The system works now and everything I set up to happen upon death is executed perfectly.

One last thing before I go back into getting way over my head: I'm trying to find a way to make the game check if any object of a certain class is present (the closest I've found are ways to check if particular objects are of certain classes). The reason is that I've created this:
Code:
class Monster: UntakeableActor
    LoseHP(Damage)
    {
        HP -= Damage;
    }   
    HealHP(Amount)
    {
        HP += Amount;
        HP = min(HP, MaxHP);
    }
    dobjFor(Attack)
    {
        action()
        {
            LoseHP(myATK.CurrentATK);
            "You strike the <<theName>>. ";
            if (HP <= 0)
            {
                moveInto(nil);               
                "You defeated the <<theName>>. ";
                HP = MaxHP;
            }
            else
                "\^<<theName>> has <<HP>> HP left. ";
        }
    }
;

to include all the generic behaviour for monsters.

My intention is to have the !(redSlime.getOutermostRoom() == self) part of the random encounter trigger check for the prescence of ANY object of the Monster class instead of the redSlime in particular. There are several reasons for this, but the most imperative is so that I can add a second random roll to select from a pool of monsters to summon, instead of a particular one. Something like
Code:
        local roll = rand(100);
        if (roll <= 20)
            redSlime.moveInto(self);
        if (roll >= 21 && <= 40)
            someOtherMonster.moveInto(self);
        if (roll >= 41 && <= 60)
            And so on and so forth


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 5:35 pm 
Offline

Joined: Tue Apr 27, 2010 1:02 pm
Posts: 797
Thanks for the fixes, Pacian. That's what I get for posting untested code (and working with languages other than TADS).

Quote:
I'm trying to find a way to make the game check if any object of a certain class is present (the closest I've found are ways to check if particular objects are of certain classes).


My usual way of handling this is to maintain lists of significant class objects. I do this with a helper list object created by this macro:

Rant: show
Code:
#define MakeClassList(K, N) \
##N : object \
 \
    lst() \
    { \
    if (lst_ == nil) \
        initLst(); \
    return lst_; \
    } \
 \
    initLst() \
    { \
        lst_ = new Vector(50); \
        local obj = firstObj(); \
        while (obj != nil) \
        { \
            if(obj.ofKind(##K)) \
                lst_.append(obj); \
            obj = nextObj(obj); \
        } \
        lst_ = lst_.toList(); \
    } \
 \
    indexOf(val) \
    { \
        if (lst_ == nil) \
            initLst(); \
        return lst_.indexOf(val); \
    } \
 \
   indexWhich(func) \
   { \
      if (lst_ == nil) \
         initLst(); \
      return lst_.indexWhich(func); \
   } \
 \
    subset(func) \
    { \
        if (lst_ == nil) \
            initLst(); \
        return lst_.subset(func); \
    } \
 \
    lst_ = nil \


This is complete overkill if all you want is a list of monster objects, but comes in handy if you also need lists of armor, weapons, rooms, and so forth.

With that macro in place, your code becomes something like this:

Code:
MakeClassList(Monster, allMonsters);

randomEncounterRoom: Room 'Random room'
   "This is the random encounter room. "

   enteringRoom(traveler)
   {
      inherited(traveler);
      
      // yields a number between 0 and 99
      local roll = rand(100);

      // % chance of random encounter
      local freq = 80;

      if (roll < freq && !(allMonsters.indexWhich({x: x.getOutermostRoom() == self})))
      {
         local roll = rand(100);
         
         if (roll < 33)
            redSlime.moveInto(self);
         else if (roll < 66)
            blueSlime.moveInto(self);
         else
            greenSlime.moveInto(self);   
      }
   }
;

class Monster: UntakeableActor;

redSlime: Monster 'red slime' 'RED SLIME';
blueSlime: Monster 'blue slime' 'BLUE SLIME';
greenSlime: Monster 'green slime' 'GREEN SLIME';


If the first random roll succeeds and there are no monsters in the room, the second roll will randomly pick one of the slimes to teleport in.

To generalize this a little more, you could have a RandomMonster subclass of Monster and make your random encounters descend from that class. Then instead of rolling 100 and breaking down the choice by hand, you could roll for the length of the allRandomMonsters list, and summon the monster corresponding to the index of the rolled value in the list.


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 9:32 pm 
Offline

Joined: Sun Jan 30, 2011 10:20 pm
Posts: 32
bcressey wrote:
This is complete overkill if all you want is a list of monster objects, but comes in handy if you also need lists of armor, weapons, rooms, and so forth.

Yes, I can already see how that’ll come in handy. Thank you very much.

bcressey wrote:
To generalize this a little more, you could have a RandomMonster subclass of Monster and make your random encounters descend from that class. Then instead of rolling 100 and breaking down the choice by hand, you could roll for the length of the allRandomMonsters list, and summon the monster corresponding to the index of the rolled value in the list.

I did that but with a Room class. That way I can easily designate whole areas to have the exact same monster encounters:

Finally, I really wanted to say that “This will last me for a while”; but I ran into another weird problem right away:

This time, it was implementing a system for accuracy and dodging attacks. Its rather simple, I just gave the player character a numerical property to designate his accuracy:
Code:
+myACC: Component 'my accuracy'
    desc = "ACC: <<CurrentACC>> "
    CurrentACC = 100
;

I gave the monster another to designate its speed:
Code:
slime: Monster 'blue slime monster' 'Slime' @activeBattleRoom
    "It looks like a small gelatinous puddle of blue sludge with eyes. "
    HP = 1000
    MaxHP = 1000
    ATK = 100
    SPD = 10 //Here
;

And then added the following check to the dobjFor(Attack) method in the Monster class definition:
Code:
            local roll = rand(100);
            local hitChance = (myACC.CurrentACC -= SPD);
            if (hitChance >= roll)

So it looks like this:
Code:
dobjFor(Attack)
    {
        action()
        {
            local roll = rand(100);
            local hitChance = (myACC.CurrentACC -= SPD);
            if (hitChance >= roll)
            {
                LoseHP(myATK.CurrentATK);
                "You strike the <<theName>>. ";
                if (HP <= 0)
                {
                    moveInto(nil);               
                    "You defeated the <<theName>>. ";
                    HP = MaxHP;
                }
                else
                    "\^<<theName>> has <<HP>> HP left. ";
            }
            else
                "\^<<theName>> dodged your attack! ";
        }
    }

The formula should be rather simple: Get a random roll between 0 and 99, then subtract the target monster’s speed (10) from the player’s accuracy (100) to get the hit chance value. If the hit chance value is lower than the roll, the attack misses. Otherwise, it hits and the attack method triggers as before. With these values, any roll between 0 and 90 will ensure a hit, whereas anything between 91 and 99 will be a miss.

Also, I did call the randomize() function on game startup:
Code:
gameMain: GameMainDef
    showIntro()
    {
        "Welcome to the testing arena for a theoretical combat system. Soon to be Spaghetti Code Clusterfuck. ";
        randomize();
    }
    initialPlayerChar = me
;


It works… at first. I hit most of the time and miss others, but eventually I miss and I just keep missing forever in statistically impossible ways. Somehow, the roll result gets jammed at >90 at some point. No matter what, I never hit after the 9th attack.

So I’m drawing a blank on what-NEVERMIND, I'M A MORON, I GOT THE PROBLEM, I just don't know how to solve it even though it's probably really simple. Obviously, each time I call
Code:
local hitChance = (myACC.CurrentACC -= SPD);

the game actually sustracts SPD from myACC.CurrentACC instead of using them as stable values, so obviously I can't hit past the ninth because by then myACC.CurrentACC = 10 and so myACC.CurrentACC -= SPD is always 10 -= 10, which is obviously going to be lower than the roll... problem is, I don't recall how to tell the game to just use those values to get a number instead of changing them.


Top
 Profile Send private message  
 
PostPosted: Sun May 01, 2011 11:22 pm 
Offline

Joined: Tue Apr 27, 2010 1:02 pm
Posts: 797
You want this instead:

Code:
local hitChance = myACC.CurrentACC - SPD;


Top
 Profile Send private message  
 
PostPosted: Mon May 02, 2011 6:11 am 
Offline

Joined: Sun Jan 30, 2011 10:20 pm
Posts: 32
I feel both stupid and grateful. Thank you.


Top
 Profile Send private message  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 10 posts ] 

All times are UTC - 6 hours [ DST ]


Who is online

Users browsing this forum: No registered users and 1 guest


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group