Jump to content

Of creature scripts


Archaon

Recommended Posts

Once again I’ve tried something ambitious and once again I’m only partly successful.

Take a load of this:

Click to reveal..
Code:
//Crtele v0.3//A departure from the basic script. The creature wraps next to each character//if able and attacks. //   Cell 1,2 - Stuff done flag. If both 0, nothing. Otherwise when this //     is killed, set to 1. //   Cell 3 - Dialogue node to start with if talked to. if left at 0, this//     character doesn't talk.begincreaturescript;variables;short i,x,y,xa,ya;body;beginstate INIT_STATE;	break;beginstate DEAD_STATE;	// Set the appropriate stuff done flag for this character being dead	if ((get_memory_cell(1) != 0) || (get_memory_cell(2) != 0))		set_flag(get_memory_cell(1),get_memory_cell(2),1);break;beginstate START_STATE; //the only state used  	i = 0;	set_aggression(ME,100);	set_strategy(ME,10);//target does not change automatically	set_char_status(ME,3,15,1,0); //Haste it for more action points	while (i < 6){ //once for each party member	   if (char_ok(i)){ //make sure character exists   	      if ((dist_to_char(i) <= 16) && (can_see_char(i))){ //Both distance and LOS	         x = char_loc_x(i);//store x,y		     y = char_loc_y(i); 		     xa = x;                     ya = y;			 //check terrain around target		     if ((get_terrain(x, y + 1) == 0) && (char_on_loc(x,y + 1) == -1))		         ya = y + 1;				             if ((get_terrain(x, y - 1) == 0) && (char_on_loc(x,y - 1) == -1))		         ya = y - 1;						     if ((get_terrain(x + 1, y ) == 0) && (char_on_loc(x + 1,y) == -1)){		         xa = x + 1;				 ya = y;			 }		     if ((get_terrain(x - 1, y ) == 0) && (char_on_loc(x - 1,y) == -1)){		         xa = x - 1;				 ya = y;			 }	         if ((xa != x) || (ya != y)){ //if destination found...		         if ((xa != my_loc_x()) || (ya != my_loc_y())){//teleport if I'm not there...		            put_boom_on_char(ME,2,0);					force_instant_terrain_redraw();			        play_sound(-10);					pause(2);		            relocate_character(char_on_spot(my_loc_x(),my_loc_y()),xa,ya);  //convulted, but relocate_character does not accept 'ME'			        put_boom_on_char(ME,2,0);					force_instant_terrain_redraw();					pause(2);		         }		 	     set_target(ME,i);//target...			     do_attack();//attack!		     }		   }  	    }		i = i + 1;//repeat for each party member, if possible	}		end_combat_turn();//then end turn.	break;beginstate TALKING_STATE;	if (get_memory_cell(3) == 0) {		print_str("Talking: It doesn't respond.");		end();		}	begin_talk_mode(get_memory_cell(3));break;

 

A creature script that wraps around the party instead of moving normally. It’s also supposed to attack all characters each turn, although it only manages none to 3 for some reason.

Problems? Some graphic glitches. Do_attack() not always working, even though I force aggression to 100 and provide valid targets. Action points seem to cap at 10, even with haste, while I need at least 13 for 4 attacks. Perhaps do away with do_attack and deal damage manually?

Thoughts? Testing? Debugging? Suggestions? Spam?

Link to comment
Share on other sites

The basic idea of the script looks sound to me. I'll do some tests later to see what I can figure out. At a glance, Id suggest adding else's to the conditional tree in the section labeled '//check terrain around target' so that if you find a clear space you don't waste time checking the other possibilities, but that's a minor detail.

 

Haste only adds a few extra AP (two right?), so I assume you're also using the bonus_ap setting in the creature definition. If I recall correctly, the Divinely Touched status also confers extra AP, so maybe that would help.

 

do_attack() can be frustrating because it contains all of its own logic, which you'll have noticed includes a lot more than just attacking; for this purpose it might be easier to just inflict damage directly, but it seems like you've set up a situation in which do_attack() ought to get the idea of what you expect it to do. I doubt that aggression and courage are a problem since they default to 100 anyway and you're not using the built-in target selection logic, which is about the only place aggression matters.

Link to comment
Share on other sites

I tinkered around a bit more. Divinely touched gave a whopping 18 AP. The script also managed to damage 4 characters… once out of 30 or so turns. Scuttling do_attack seems more and more appealing. It seems to average at 50% success, for some reason. The probability distribution feels like a bell curve…

 

The graphic bug is even weirder. Each time the creature teleports from a position without successfully dealing damage, the first frame of the teleport explosion stays still in each place. The next time anyone lands an attack, all animations resolve in bulk. I have tried to run force_instant_terrain_redraw in multiple places to no avail. It’s not even because of speed. I tried putting large pauses, again with no change.

 

On a slightly unrelated note, is there a way to determine the direction a character faces? There is a cutscene call to set the direction but nothing that returns it.

Link to comment
Share on other sites

To go with put_boom_on_char() you'll want run_animation() rather than force_instant_terrain_redraw(), I think. You're getting funny behavior because the latter doesn't know about the subsequent frames of the animation. When the creature manages to land a blow the game engine runs the animation mechanism, so your leftover animations are getting noticed and taken care of then.

 

Quote:
On a slightly unrelated note, is there a way to determine the direction a character faces? There is a cutscene call to set the direction but nothing that returns it.

There is no officially sanctioned way to do this. It is. . . possible, but not pretty, and not portable.

Link to comment
Share on other sites

If you put this in the creature's START_STATE, as soon as the monster's script starts running, it will run all of its movement code and try to attack the party. Might I suggest that you put this code in state 3 instead, and include a last_abil_time check?

 

And you should definitely use "run_animation_sound(10)" to play your animations and sound instead of play_sound and force_instant_terrain_redraw. I also agree that do_attack is useless; it's the part of the game that Jeff didn't allow designers to modify, so naturally it's horrible. I would suggest using damage_char() or change_char_health() instead.

 

I wrote a script that allows the monster to jump from one target to the next, and damages his targets as he jumps around. It has sound effects, zaps, and all the bells and whistles. I suggest that you check it out. If you don't want to, I'm pasting the relevant section of code anyway. This is what the code runs for character a:

Code:
while (ready == 0 && e < 60) {	x_to_place = char_loc_x(a) - 1;	y_to_place = char_loc_y(a) - 1;	x_to_place = get_ran(1,0,2) + x_to_place;	y_to_place = get_ran(1,0,2) + y_to_place;	if (char_on_loc(x_to_place,y_to_place) <= -1)		ready = 1;	e = e + 1;	}if (ready == 1) {	change_char_health(a,-1 * get_ran(2,1,12));	put_jagged_zap(my_loc_x(),my_loc_y(),x_to_place,y_to_place,1);	put_jagged_zap(my_loc_x(),my_loc_y(),x_to_place,y_to_place,1);	put_jagged_zap(my_loc_x(),my_loc_y(),x_to_place,y_to_place,1);	put_boom_on_char(ME,2,0);	relocate_character(my_number(),x_to_place,y_to_place);	run_animation_sound(71);	set_character_pose(ME,2);	set_character_facing(ME,get_ran(1,0,7));	force_instant_terrain_redraw();	set_character_pose(ME,1);	force_instant_terrain_redraw();	f = 1;	}
Link to comment
Share on other sites

I did see the script you mention (something massacre, I think. I haven’t played your scenario yet, but I soon will.).

 

If I read this correctly, the last_abil method uses the wrap every other turn or so. I intended that script to be used to the exclusion of all other attacks (So, the enemy has one powerful ability, but no versatility.)

 

However I must confess your method of finding a free space (the first ‘while’ in the previous post) makes my skin crawl. Brute-forcing random numbers in hopes of a match is frankly a bit horrible (yet still better than what I did in my first version.)

So, I came up with this:

Code:
x = 0;y = 0;i = 0;ready = 0p = get_rand(1,1,8);while ((i < 9) && (ready == 0)){   if ((p < 3) && (p > 7))     y = -1;   if ((p > 3) && (p < 7))     y = 1;   if ((p > 1) && (p < 5))     x = -1;   if ((p > 5) && (p < 9))     x = 1;   if (char_on_loc((char_loc_x(a) + x),(char_loc_y(a) + y)) = -1)     ready = 1;   i = i + 1;   p = p + 1;   if (p > 8)      p = 1;} 

A bit more complicated, so I’ll explain. There are 8 possible positions around the target. The ‘p’ variable represents that, with 1 for north and moving counter-clockwise.

 

X and y are the offsets from the target’s position. Putting the aforementioned 8 positions on a 3x3 grid, you’ll notice that for each side of the grid, one of the offsets always changes the same way, to either 1 or -1. The string of double condition ifs calculates that.

 

So we have a line of conditions that depend on ‘p’ and a while loop that runs 8 times at most. By making the while’s control variable different from the ‘p’ and by selecting the ‘p’ randomly, you ensure the check will start from a random point, and thus the result will be random.

 

At the end of the loop, the ‘p’ cycles back to 1 if it’s larger than 8, so that all positions will be checked.

Link to comment
Share on other sites

Quote:
Brute-forcing random numbers in hopes of a match is frankly a bit horrible (yet still better than what I did in my first version.)

There was some discussion of this during testing. It turns out that it actually works passably assuming that the random number generator results are fairly well distributed; being capped at 60 iterations it will try at most 60 times, rather than the optimal 8, and has a probability of .9997 (= 1-(7/8)^60) of finding a solution in the case where there is only one free space.

The method I proposed for this was:
Code:
ready = 0;i = 0;offset = get_ran(1,0,8); //or whatever other variable you have handy at that pointwhile(i<9 && ready==0){	if(i!=4){		x_to_place = char_loc_x(a)+(((i+offset)%9)%3)-1;		y_to_place = char_loc_x(a)+(((i+offset)%9)/3)-1;		if (char_on_loc(x_to_place,y_to_place) <= -1)			ready = 1;	}	i = i + 1;}


This is basically the same idea as you propose, arranged differently. (Although I don't see why you loop would execute 8 times in the worst case, it looks like it would be 9 times, since i is allowed to be in the range [0,8].)
Link to comment
Share on other sites

Don’t worry, we are all bad people here, coding in obscure pseudo-C for no good reason and forcing poor creatures to follow our whims. XD

 

Anyway, the above code was written on notepad, on a public PC, with no way to test it. I managed to integrate it into my original code after some much-needed debugging and testing. (And yes, now it runs 8 times, like it should ;-) Run_animation_sound worked like a charm. I also wrapped it all in a conditional, so that it only starts wrapping around when it has a target. Here’s the current, actually working version:

 

Click to reveal..
Code:
 //Crtele v0.8//A departure from the basic script. The creature wraps next to each character//if able and attacks. //   Cell 1,2 - Stuff done flag. If both 0, nothing. Otherwise when this //     is killed, set to 1. //   Cell 3 - Dialogue node to start with if talked to. if left at 0, this//     character doesn't talk.begincreaturescript;variables;short i,j,x,y,a,ready, h, ap;int g;body;beginstate INIT_STATE;	break;beginstate DEAD_STATE;	// Set the appropriate stuff done flag for this character being dead	if ((get_memory_cell(1) != 0) || (get_memory_cell(2) != 0))		set_flag(get_memory_cell(1),get_memory_cell(2),1);break;beginstate START_STATE; //the only state usedif (get_attitude(ME) > 4){ //only run if hostile to party  	i = 0;	set_strategy(ME,10);//target does not change automatically	set_char_status(ME,3,3,1,0); //Haste it for more action points	set_char_status(ME,16,3,1,0); //And divinely touch it	while (i < 6){ //once for each party member     	   if (char_ok(i)){ //make sure character exists   	      if ((dist_to_char(i) <= 16) && (can_see_char(i))){ //Both distance and LOS			 //check terrain around target and select destination			 h = get_height(char_loc_x(i), char_loc_y(i));                       j = 1;             ready = 0;             a = get_ran(1,1,8); //random position around target. 1 for north, c-clockwise.             while ((j < 9) && (ready == 0)){               x = 0;               y = 0;               if ((a < 3) && (a > 7))                  y = -1;               if ((a > 3) && (a < 7))                  y = 1;               if ((a > 1) && (a < 5))                  x = -1;               if ((a > 5) && (a < 9))                  x = 1;               x = char_loc_x(i) + x;               y = char_loc_y(i) + y;			   //long series of checks. This can only improve by checking floor and terrain numbers (*shudder*):               if (((char_on_loc(x,y) == -1) || (char_on_loc(x,y) == my_number())) && (can_see_loc(x,y) == 1) && (get_height(x,y) == h) && (is_blocked(x,y) == 0))                  ready = 1;               j = j + 1;               a = a + 1;               if (a > 8) //We haven't checked every space yet, so cycle back to 1                a = 1;              }		     if (ready == 1){ //if destination found...		         if ((x != my_loc_x()) || (y != my_loc_y())){//teleport if it's not there...		            put_boom_on_char(ME,2,0);   				            relocate_character(my_number(),x,y);                    put_boom_on_char(ME,2,0);					run_animation_sound(10);			         }		 	     set_target(ME,i);//target...				 ap = my_ap();				 g = 0;				 while ((my_ap() == ap) && (g < 5)){ //Try to attack more than once. Sometimes even g < 500 doesn't work...				    do_attack();				    g = g + 1;				 			    }		   }		  }  	    }	i = i + 1;//repeat for each party member, if possible	}						end_combat_turn();//then end turn.}break;beginstate TALKING_STATE;	if (get_memory_cell(3) == 0) {		print_str("Talking: It doesn't respond.");		end();		}	begin_talk_mode(get_memory_cell(3));break; 

 

More do_attack woes. I thought I was pretty smart when I tried this:

ap = my_ap();

while (my_ap() == ap){

do_attack();

g = g + 1;

}

After generating an infinite loop error, I thought better. Sometimes, even 1000 repeats won’t result in an attack. By testing more, I found three possible cases.

-do_attack works at once

-It works after roughly 2-5 repeats

-it doesn’t work no matter how many times you spam it.

 

I can only think of theories right now. Perhaps the random generator is not seeded often enough, or perhaps a finite number of random values is pre-generated each round. Some in these forums have messed around with the BoE code. Is there something comparable there?

 

Also, did I miss some is_REALLY_ blocked call? My placement detection usually works, but it can still teleport the creature behind a window, on water, etc. Need I really use some huge get_floor/terrain conditional to make sure?

Link to comment
Share on other sites

Quote:
Also, did I miss some is_REALLY_ blocked call? My placement detection usually works, but it can still teleport the creature behind a window, on water, etc. Need I really use some huge get_floor/terrain conditional to make sure?

No and yes, unfortunately, respectively. 'blocked' in the context of change_blocked(), is_blocked(), and terrain or floor special property 6 actually means 'blocked to NPCs'. Checking for this is a good thing to do, but as you've discovered, it is necessary without being sufficient.

As for finding out whether a space is passable because of floors and terrains, you're basically on your own; the game knows, obviously, but it isn't telling. In theory this can be figured out from either the relevant floor and terrain definition, or more concisely from the blockage array the game keeps track of, but neither of these is available from a script. Probably the best you can do is to make a few floor and terrain checks, and then conservatively assume that otherwise the space isn't passable, accepting that your AI will then have a few odd behaviors like perhaps an aversion to rugs.

I'd consider the following a reasonable approximation (for a space being passable):
floor < 23
floor > 36 and floor < 57
floor != 87 and floor != 88 and floor != 255

terrain < 125
terrain > 168 and terrain < 200
terrain > 222 and terrain < 229
terrain > 288 and terrain 297

The disadvantage is that not only is it tedious to type and maintain, but it will also execute slowly due to the lack of short-circuit evaluation.
Link to comment
Share on other sites

Originally Posted By: Archaon
while ((my_ap() == ap) && (g < 5)){ //Try to attack more than once. Sometimes even g < 500 doesn't work...
do_attack();
g = g + 1;

}


Oh, that IS clever. Long live the magical while() loop.

If the creature running the script, a creature I'll call cr_tele, is standing right next to a character which it intends to attack, does that attack proceed correctly? That is, if some random monster stands next to cr_tele and they're hostile to each other and both immobile, will they attack each other properly?

I know that get_nearest_char() doesn't actually get the nearest char; it gets the nearest char at a secret time of BoA's choosing. It usually isn't a problem, unless you want to, say, relocate a character repeatedly in one combat turn. I discovered this when I tried to do get_nearest_char(), relocate_char(), and then get_nearest_char(); I got the same character twice. Do_attack might rely upon the same tracking of location that get_nearest_char makes use of, which would mean that the game believes cr_tele is in a very different location than it actually is. Or at least, the part of the game engine that handles things like get_nearest_char and do_attack.

I suppose that, if my theory is true, we should be thankful that relocate_char() works at all.

It might be possible to force BoA to reset its tracking of characters, but I have no idea how; maybe placing and then removing terrain, or placing and then deleting a character? Assigning a waypoint for the monster to move to? But overall, I think do_attack() is more trouble than it's worth. If you get it working, you might find that it operates slowly due to the game's inefficient running of the script, or because it takes a while to play the animations and sounds. So I still suggest change_char_health(), run_animation_sound(), set_char_pose(), and force_instant_terrain_redraw() in order to achieve an effect that is like an attack, but which gives you more control.
Link to comment
Share on other sites

That piece of runtime stupidity is good to know. It’s mostly avertable, too, if you nest deeply and only use 1 or 2 conditions per line. (Which, on the other hand, inflates your node total. *sigh*) Is there a nest limit? I have yet to hit one, though I haven’t really tried.

 

Of course, one could also give property 6 to all appropriate floors and terrains and make is_blocked actually useful. A lot of work perhaps, but it only needs to be done once. If the shoe doesn’t fit, mutate the toe!

 

@Metatron: To test the script, I gradually spread my party all over my testing town. I have seen attacks succeeding after a 10+ jump and failing after a 2- jump. It isn’t really consistent. Also, I in no way prevent the creature from moving around on its own. Do_attack is supposed to make it actually move towards the target if needed, but it has never done so, which means that at least part of it realizes the set target is right next to it. Schizoid code? Schrödinger’s attack? Can’t tell yet…

 

The problem with emulating an attack is making it balanced. It’s one thing having it wtfpwn other creatures and quite another having it attacking the party.

 

Next test: Shoving set_target in the while loop…

Link to comment
Share on other sites

Quote:
Is there a nest limit?

No, not explicitly, although with every level of nesting costs you nodes and execution speed, though not as much of the latter as if it's helping to short circuit a long calculation. Fun fact: Every time the interpreter hits a right bracket, it scans backward through the script to find the matching left bracket, so that it can find out whether it's just finished a while loop body and needs to jump back to evaluate the condition. If in fact the bracket belonged to an if or else, the exercise is a waste of time.*

Quote:
Of course, one could also give property 6 to all appropriate floors and terrains and make is_blocked actually useful.

Now that's a clever idea. Where you would really gain by doing this would be on the terrains, since there are more of them to begin with and the passable, and impassable groups are more jumbled.

*This analysis comes with the usual disclaimer that it depends on the actual game interpreter being roughly equivalent to the partial interpreter found in the editor code.
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...