Events upon room entrance and randomiizing stuff

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][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/pointpoints’ ‘your HP’
desc = “Current Health: <>”
CurrentHP = 2000
LoseHP(Damage)
{
CurrentHP -= Damage;
if (CurrentHP <= 0)
finishGameMsg(‘You lose!’, [finishOptionUndo]);
}
;
+myATK: Component 'my attack/point
points’
desc = “ATK: <>”
CurrentATK = 100
;[/code][/rant]

Slime here :[rant][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 left. ";
}
}
;

+slimeAttackAgenda: AgendaItem
isReady = (inherited() && me.location == slime.location)
initiallyActive = true
invokeItem()
{
"The Slime charges at you! ";
myHP.LoseHP(slime.ATK);
}
;[/code][/rant]
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 left. ";
}
}
;

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

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.

Something like this should work.

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);
	}
;

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.

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:

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

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);
}
;[/code]

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.

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:

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

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

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. :confused:

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:

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

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

Thanks for the fixes, Pacian. That’s what I get for posting untested code (and working with languages other than TADS).

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]#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 \ [/rant]

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:

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.

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

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:

+myACC: Component 'my accuracy' desc = "ACC: <<CurrentACC>> " CurrentACC = 100 ;
I gave the monster another to designate its speed:

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:

            local roll = rand(100);
            local hitChance = (myACC.CurrentACC -= SPD);
            if (hitChance >= roll)

So it looks like this:

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:

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

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.

You want this instead:

local hitChance = myACC.CurrentACC - SPD;

I feel both stupid and grateful. Thank you.