Morrowing

This month I have prepared a bigger Morrowing chunk for you. The main features are a new version of the skeleton enemy, a text processing system and many more. The good looking level was donated by Lennart Hillen and Dennis Fantasy. Let's see what the script files do:

- desert.wdl includes functions for the terrain detail mapping, vegetation and water;

- environment.wdl takes care of the sky cube and the rotating clouds;

- lines.wdl includes the lines (only 3 lines at the moment) that are said by the skeleton and the player;

- morrowing.wdl is the main script file; it includes the mouse movement code;

- panelstexts.wdl contains the definitions for all the panels and text that are needed by the dialog system;

- particles.wdl contains the code that makes the player bleed some particles;

- player.wdl contains the code for the player and its camera(s);

- skeleton.wdl contains the improved version of the enemy code from Aum12;

- textprocess.wdl contains a few functions that split longer texts in an intelligent manner.

text theysay_txt
{
pos_x = 0;
pos_y = 0;
layer = 20;
font = adventure_font;
}

text isay1_txt // the first line for the player
{
pos_x = 20;
pos_y = 717;
layer = 20;
string = line2_str;
font = adventure_font;
}

text isay2_txt // the second line for the player
{
pos_x = 20;
pos_y = 745;
layer = 20;
string = line3_str;
font = adventure_font;
}

These texts are displayed on the screen when the enemies and the player have something to say. I'm using only 2 texts for the player at the moment, but I'll probably use up to 5 texts (5 answers to choose from) in the end.

panel their_pan
{
bmap = their_pcx;
pos_x = 0;
pos_y = 0;
layer = 15;
flags = overlay, refresh;
}

panel my_pan
{
bmap = my_pcx;
pos_x = 0;
pos_y = 704;
layer = 15;
button = 10, 7, button2_pcx, button1_pcx, button2_pcx, choose_answer, null, null;
button = 10, 35, button2_pcx, button1_pcx, button2_pcx, choose_answer, null, null;
flags = overlay, refresh;
}

The texts appear over these panels because they have a bigger layer; I'm using a button for each line of the player.

function choose_answer(number)
{
if (number == 1) // the player has picked the "My name is The mighty Guard..." line
{
isay1_txt.visible = off;
isay2_txt.visible = off;
theysay_txt.visible = off; // hides their_pan as well -> resumes the gameplay
show_pointer = 0; // hide the mouse pointer
}
if (number == 2) // the player has picked the "All of the sudden..." line
{
exit; // shut down the engine
}
}

If the player clicks the first button on my_pan (number = 1), it chooses the line2_str string inside the lines.wdl file ("My name is The mighty Guard...") so it resumes the gameplay; therefore, the code will hide the panels, the texts and the mouse pointer. If the player clicks the second button, it has picked the line3_str ("All of the sudden...") so the engine will be shut down.

function particle_blood()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(1) - 1.5;
vec_add (my.vel_x, temp);
my.alpha = 70 + random(30);
my.bmap = blood_map;
my.size = 6;
my.flare = on;
my.bright = on;
my.move = on;
my.lifespan = 20;
my.function = fade_particle;
}

function fade_particle()
{
my.alpha -= 5 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

This is the particle function that is run whenever the skeleton manages to stab the player with its sword; you've seen it many times in Aum so I won't describe how it works again. Oh, and the player can die now!

define sword_base = skill12;
define sword_tip = skill15;

var skeletons_up = 0; // doesn't allow more than 1 skeleton to get up

action skeleton1 // attached to the first enemy that appears in the desert level
{
var skeleton_speed;
var trace_temp;
my.health = 100; // each skeleton has 100 health points
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) >= 400)
{
ent_cycle("death", 90); // set one of the last "death" frames for the skeleton
wait (1);
}

Action skeleton1 is attached to all the skeletons in the level; however, only one of them will stand up, talk and fight the player. Each skeleton has 100 health points (just another name for skill40) and lies down in one of its "death" animation frames until the player comes closer than 400 quants to it.

while (my.health > 0)
{
if ((vec_dist (my.x, player.x) < 400) && (player.health > 0))
{
if ((skeletons_up > 0) && (my.skill1 == 0))
{
my.passable = on;
return;
}
skeletons_up += 1;
my.skill1 += 1;
if (my.skill1 == 1)
{
snd_play (growl_wakeup_wav, 100, 0);
my.skill22 = 90;
while (my.skill22 > 1)
{
ent_cycle("death", my.skill22);
my.skill22 -= 3 * time;
wait (1);
}
// text processing starts here
str_cpy(temp_str, line1_str); // that's the "Why do you bother me? I'm trying to... " line
process_their_strings();
wait (1);
theysay_txt.string = their_lines_str;
theysay_txt.visible = on;
wait (3);
stop_player = 1; // stop the player for now
sleep (2); // wait for 2 seconds
isay1_txt.visible = on; // give the player the possibility to answer (this will bring up the my_pan panel as well)
isay2_txt.visible = on;
show_pointer = 1; // display the mouse pointer
while (their_pan.visible == on) {wait (1);} // stop the skeleton until this panel disappears
stop_player = 0; // allow the player to move again
// text processing ends here
}

As long as the skeleton is alive, if the player is alive and comes closer than 400 quants to the skeleton, if one of the skeletons got up already, the rest of the skeletons will become passable and will get out of the action, doing nothing from now on. If no skeleton has got up, this sack of bones will get to face the player! It increments the skeleton_up variable, as well as its skill1, and if this happens for the first time, it plays the growl_wakeup_wav sound, and then it plays the reversed "death" animation, which makes it stand up. We set the "Why do you bother me..." line of text for the skeleton, we process it (we'll discuss that function a bit later), and then we make the enemy text visible. We stop the player, we give it 2 seconds to read the text, and then we show player's texts (answers), as well as the mouse pointer. The skeleton and the player will resume their activities when the their_pan panel will disappear.

vec_set(temp, player.x);
vec_sub(temp, my.x);
vec_to_angle(my.pan, temp);
skeleton_speed.x = 15 * time;
skeleton_speed.y = 0;
vec_set (trace_temp, my.x);
trace_temp.z -= 10000;
trace_mode = ignore_me + use_box;
skeleton_speed.z = -trace (my.x, trace_temp.x) - 1;
move_mode = ignore_passable + ignore_passents;
ent_move(skeleton_speed, nullvector);
ent_cycle("walk", my.skill19);
my.tilt = 0;
my.skill19 += 7 * time;
my.skill19 %= 100;
if (vec_dist (my.x, player.x) < 80)
{
my.skill20 = 0;
snd_play (growl_attack_wav, 30, 0);
while (my.skill20 < 100)
{
ent_vertex(my.sword_tip, 291);
ent_vertex(my.sword_base, 306);
trace_mode = ignore_me + ignore_passable;
trace (my.sword_base, my.sword_tip);
if (result != 0)
{
effect (particle_blood, 2, target, normal);
if (you != null) {you.health -= 4 * time;}
ent_playsound (my, sword_wav, 50);
}
ent_cycle("attack", my.skill20);
my.skill20 += 5 * time;
wait (1);
}
sleep (0.1);
}
}

The battle is ready to begin! We rotate the skeleton towards the player and then we move it towards it with the speed given by 15 * time. The skeleton knows how to adjust its height to its surroundings and moves ignoring the passable entities. If the player is closer than 80 quants to the skeleton, it will be attacked. We trace between the sword tip and the sword base in a while loop, and if we have hit something we create a particle effect, we decrease the health of the entity that was traced, and then we play a sword_wav sound. The skeleton will play its "attack" animation in a loop for as long as the player is within its reach.

else // the player is farther than 400 quants away (or dead)
{
ent_cycle("stand", my.skill21);
my.skill21 += 2 * time;
my.skill21 %= 100;
}
wait (1);
}
while (my.skill22 < 80)
{
ent_cycle("death", my.skill22);
my.skill22 += 1 * time;
wait (1);
}
my.passable = on;
}

If the player is farther than 200 quants away (or dead!) the skeleton will play its "stand" animation in a loop; if the skeleton dies, it plays its "death" animation once, and then it becomes passable.

We are now going to discuss what's happening inside the textprocess.wdl file. These script measure the length of the enemy line and set the proper scale for the panels, split the text on 1 or 2 rows, and so on.

string temp_str = " "; // holds up to 140 characters
string my_lines_str = " "; // holds up to 140 characters
string their_lines_str = " "; // holds up to 140 characters
string sliced_str = " "; // holds up to 140 characters

I have defined 4 strings and each one of them can hold up to 140 characters; feel free to increase this limit if you want to.

starter keep_text_on_panel()
{
while (1)
{
if (theysay_txt.visible == on)
{
their_pan.visible = on;
}
else
{
their_pan.visible = off;
}
if (isay1_txt.visible == on)
{
my_pan.visible = on;
}
else
{
my_pan.visible = off;
}
wait (1);
}
}

The function above makes sure that if the enemy says something (theysay_txt.visible = on), the corresponding panel is displayed automatically. The same thing happens if the first answer that is to be picked by the player (isay1_txt) is visible. This makes the things much more easier for us; we won't have to think about the needed panels from now on.

starter align_panels()
{
while (1)
{
my_pan.pos_x = (screen_size.x - bmap_width(my_pcx) * my_pan.scale_x) / 2;
their_pan.pos_x = (screen_size.x - bmap_width(their_pcx) * their_pan.scale_x) / 2;
wait (1);
}
}

The function above centers the two panels (for the player and for the enemy) regardless of their size and scale.

function process_their_strings()
{
number_of_characters = str_len(temp_str);
if (number_of_characters < 70)
{
their_pan.scale_x = 1.1 * number_of_characters / 17;
theysay_txt.pos_x = 2 + (((screen_size.x / theysay_txt.char_x) - number_of_characters) / 2) * theysay_txt.char_x;
theysay_txt.pos_y = their_pan.pos_y + 10;
str_cpy(their_lines_str, temp_str);
}

This function processes all the lines that are to be said by the skeleton. If the current line (stored in temp_str) has less than 70 characters, their_pan is scaled accordingly, in order to fit the size of the text. I have used a 256x32 pixels panel; when its scale_x is set to 1, it can hold up to 17 characters from my adventure_font. I have added a 10% increase (I multiply by 1.1) to the panel scale to be on the safe side. We center the text on the screen, allowing a 2 pixels border on the left side and counting the number of characters that would fit on the screen using the adventure_font font. We allow a 10 pixels border on the y axis because the bitmap that is used for the panel is pretty tick for a single row of text. Here are some examples with strings that are processed automatically:

if ((number_of_characters >= 70) && (number_of_characters <= 140)) // we've got two rows of text here
{
their_pan.scale_x = 1.1 * number_of_characters / 34; // we are going to have 2 rows, so we divide by 17 * 2
their_pan.scale_y = 1.4;
temp_var = number_of_characters * 0.47; // try to get a value that's close to 50% of the string
str_cpy(sliced_str, temp_str); // don't destroy the content of temp_str
str_clip(sliced_str, temp_var);
while (str_to_asc(sliced_str) != 32) // loop until you have found a space (the character that separates 2 words)
{
str_clip(sliced_str, 1);
temp_var += 1;
}
// got the area that is closed to the middle and includes a "space" character (got the splitting point for the string)
str_cpy(their_lines_str, temp_str);
str_trunc(their_lines_str, (number_of_characters - temp_var)); // get rid of the tail
str_cat(their_lines_str, "\n");
str_cpy(sliced_str, temp_str);
str_clip(sliced_str, (temp_var + 1)); // get rid of the "space" character that was separating the 2 words because we don't need it
str_cat(their_lines_str, sliced_str);
theysay_txt.pos_x = 2 + (((screen_size.x / theysay_txt.char_x) - number_of_characters / 2) / 2) * theysay_txt.char_x; // center the text on the screen
theysay_txt.pos_y = their_pan.pos_y + 8;
}
}

If the line stored in temp_str has more than 70 characters, the text will be split in 2 rows. We set the proper scales for their_pan, and then we compute a value for temp_var that is close to 50% of the string. We copy temp_str to sliced_str, and then we remove the first temp_var characters. We loop until we find the following "space" character in the string, because the text must be split on 2 rows nicely, without cutting the words in two slices. As soon as we have found the "space" character that is close to the middle of the string, we get rid of the tail (all the characters after "space"), we insert a "\n" sequence, which tells the engine to move to the second row, and then we glue the last part of the string back. We center the text on the screen and we set a 8 pixels border between the text and its corresponding panel. Sounds pretty complicated, but we won't need to adjust the texts, panels, lines, and so on because all these operations will be performed for us by the function above.