Morrowing

In diesem Monat gibt es einen etwas größeren Morrowing-Brocken. Die Haupt-Features sind eine neue Version des Skelett-Gegners, ein System um Text zu verarbeiten und vieles mehr. Das gut aussehende Level ist von Lennart Hillen und Dennis Fantasy beigesteuert worden. Schauen wir uns die einzelnen Skript-Dateien an:
- Desert.wdl enthält Funktionen für das Detail Mapping des Terrains, Vegetation und Wasser;
- Environment.wdl kümmert sich um den Sky Cube und die Wolken;
- Lines.wdl enthält die Zeilen (im Moment erst 3), die das Skelett und der Spieler sagen;
- Morrowing.wdl ist die Hauptdatei, sie enthält den Code für die Maus;
- Panelstexts.wdl enthält Definitionen für die Panel und Textobjekte für das Dialogsystem;
- Particles.wdl enthält Code, der den Spieler bluten läßt;
- Player.wdl ist der Code für den Spieler und die Kamera(s);
- Skeleton.wdl enthält eine verbesserte Version des Gegnercodes aus AUM 12;
- Textprocess.wdl enthält Funktionen, die längeren Text intelligent aufteilen.

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

Diese Texte werden angezeigt, wenn die Gegner und der Spieler etwas zu sagen haben. Bisher verwende ich nur 2 Texte für den Spieler, aber am Ende werde ich vermutlich bis zu 5 haben (5 Antwortmöglichkeiten).

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

Die Texte erscheinen über den Panels, weil ihr Layer höher ist; für jede Zeile des Spielers habe ich einen Button.

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

Falls der Spieler den ersten Knopf auf my_pan drückt (number = 1), wurde line2_str aus lines.wdl ausgewählt (“My name is The mighty Guard...”), also wird mit dem Spiel fortgefahren; daher verbirgt der Code die Panels, Texte und den Mauszeiger. Wird der zweite Knopf gedrückt, hat der Spieler line3_str ausgewählt (“All of the sudden...”), daher wird die Engine beendet.

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

Diese Partikelfunktion wird aufgerufen, wenn es dem Skelett gelingt, den Spieler mit dem Schwert zu treffen; Sie kennen diese Funktion aus so vielen Ausgaben, dass ich nicht nochmals beschreiben werde wie sie funktioniert. Oh und der Spieler kann jetzt sterben!

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

Die Action skeleton1 wird allen Skeletten im Level gegeben; allerdings wird nur eines von ihnen aufstehen und mit dem Spieler reden und ihn bekämpfen. Jedes Skelett hat 100 Health (ein anderer Name für skill40) und liegt in der “death” Animation am Boden bis der Spieler näher als 400 Quants herangekommen ist.

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
}

Solange das Skelett lebt, falls der Spieler lebt und näher als 400 Quants ist und falls bereits eines der Skelette aufgestanden ist, werden die übrigen Skelette passierbar und beenden die Action. Falls noch kein Skelett aufgestanden ist, wird dieser Knochenhaufen dem Spieler entgegentreten! Die skeleton_up Variable wird erhöht, ebenso wie skill1 und falls dies das erste Mal geschieht, erklingt der growl_wakeup_wav Sound, woraufhin die “death” Animation rückwärts abgespielt wird, wodurch das Skelett aufsteht. Wir setzen die “Why do you bother me...” Zeile als Text für das Skelett, verarbeiten diesen (dazu kommen wir gleich) und machen den Text sichtbar. Der Spieler wird angehalten, er erhält 2 Sekunden um den Text zu lesen und dann wird der Text für den Spieler (die Antwortmöglichkeiten) eingeblendet, zusammen mit dem Mauszeiger. Das Skelett und der Spieler nehmen ihre Aktionen wieder auf, wenn das their_pan Panel verschwindet.

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

Der Kampf kann beginnen! Das Skelett dreht sich zum Spieler und bewegt sich mit einer Geschwindigkeit von 15 * time auf ihn zu. Das Skelett weiß, wie es seine Höhe an die Umgebung angleicht und ignoriert passierbare Entities bei der Bewegung. Ist der Spieler näher als 80 Quants, so wird er attackiert. Wir führen einen trace zwischen der Schwertspitze und dem Knauf in einer Schleife aus und falls wir etwas treffen, wird ein Partikeleffekt erzeugt. Wir ziehen etwas von der Gesundheit der getroffenen Entity ab und lassen ein Geräusch ertönen. Das Skelett durchläuft seine “Attack”-Animation, solange der Spieler in Reichweite ist.

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

Ist der Spieler weiter als 200 Quants entfernt (oder tot!) ist das Skelett in seiner “Stand” Animation; stirbt das Skelett, wird einmal die “death” Animation abgespielt und dann ist es passierbar.

Sehen wir uns nun die textprocess.wdl Datei genauer an. Dieses Skript ermittelt die Länge von Textzeilen und wählt eine geeignete Panelskalierung und teilt den Text gegebenenfalls auf mehrere Zeilen auf.

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

Ich habe 4 Strings definiert, die jeweils 140 Zeichen aufnehmen können; wenn Sie möchten, können Sie diese Grenze ändern.

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

Die obige Funktion stellt sicher, dass sobald der Gegner etwas sagt (theysay_txt.visible = on) das entsprechende Panel automatisch angezeigt wird. Dasselbe geschieht, falls die erste Antwort des Spielers (isay1_txt) sichtbar ist. Das erleichtert die Dinge enorm, wir müssen über diese Panels von nun an nicht mehr nachdenken.

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

Diese Dunktion zentriert die Panels (für den Spieler und den Gegner) ungeachtet ihrer Größe und Skalierung.

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

Diese Funktion verarbeitet die Zeilen, die das Skelett spricht. Falls die aktuelle Zeile (in temp_str) weniger als 70 Zeichen hat, wird their_pan entsprechend skaliert, um sich dem Text anzupassen. Ich habe ein Panel mit 256 x 32 Pixeln Größe benutzt; mit einem Skalierungsfaktor von 1 passen da 17 Zeichen meines adventure_font Zeichensatzes drauf. Ich habe 10% (Multiplikation mit 1,1) zur Größe hinzugefügt, um sicherzugehen. Der Text wird zentriert, mit einem Rand von 2 Pixeln links, indem die Anzahl der Zeichen benutzt wird, die in der Schrift auf den Bildschirm passen. In y-Richtung erlauben wir 10 Pixel Rand, weil die Bitmap für das Panel recht dick für eine einzelne Textzeile ist. Hier sind einige Beispiele mit automatisch verarbeiteten Strings:

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

Falls die Zeile in temp_str länger ist als 70 Zeichen, wird der Text in 2 Zeilen aufgeteilt. Their_pan wird entsprechend skaliert und wir berechnen einen Wert für temp_var, der nah bei 50% der Stringlänge ist. Temp_str wird in sliced_str kopiert und wir entfernen die ersten temp_var Zeichen. In einer Schleife ermitteln wir das nächste Leerzeichen, weil wir den Text korrekt aufteilen möchten und nicht mitten im Wort trennen. Sobald wir ein Leerzeichen haben, welches in der Nähe der Mitte des Strings liegt, schmeißen wir den hinteren Teil weg (alle Zeichen nach dem Leerzeichen) und fügen ein “\n” ein, was für die Engine einen Zeilenumbruch bedeutet und dann kleben wir den Rest des Strings wieder an. Der Text wird zentriert und es wird ein Rand von 8 Pixeln zwischen dem Text und dem Rand des Panels gelassen. Klingt sehr kompliziert, aber wir müssen jetzt keine Texte mehr auf Panels anpassen, weil all dies von der Funktion übernommen wird.