> T E X T O P I A Einführung in die Programmierung von Text-Adventures mit Textopia 1.0 ,\=/ oo___@ ! {,,,___) \ /\ /\ (_ \/^ (/^\ ( (_ \/^\ ( (_ \ _ ( (_ \ !! \/\( (_( ! !! (__( ( )! \ /(___(/`` \ // ___( (___( )____// ;;;__ ;;;_______)_____/ Geschrieben von Oliver Berse Juni 1999 INHALT ------ I Einleitung II Kompilieren der Units 2.1 Der Quelltext 2.2 Kompilieren mit Turbo Pascal unter DOS 2.3 Kompilieren mit Delphi unter Windows 2.4 Kompilieren mit Free Pascal unter Linux 2.5 Worauf Sie unter Linux achten sollten III Textopia und OOP 3.1 Einige wichtige Begriffe 3.2 Adventures und Objekte IV Das erste Programm 4.1 Units, Konstanten, Typen und Variablen 4.2 Vorbereitungen mit Prepare 4.3 Objekte und ihre Namen: Ein erster Raum 4.4 Statische Objektbeschreibungen und Sonderzeichen 4.5 Spieler und Spiel 4.6 Ein erster Dialog V Die Spielwelt 5.1 Gegenstände und Attribute 5.2 Behälter 5.3 Fortbewegungsmittel 5.4 Weitere Räume 5.5 Variable Objektbeschreibungen 5.6 Verbindungen und Türen 5.7 Der Scope VI Aktionen und Reaktionen 6.1 Ereignisse 6.2 BeforeAction und AfterAction VII Der Parser 7.1 Die Eingabe 7.2 Verben und Grammatik 7.3 TEvent und TNoun 7.4 Fehlermeldungen 7.5 Metaverben 7.6 Debugkommandos VIII Verschiedenes 8.1 Nichtspielercharaktere 8.2 Leben, Tod und Sieg des Spielers 8.3 Hintergrundprozesse, Zeitschalter und Spielzeit 8.4 Licht und Dunkel 8.5 Die Spielstandverwaltung 8.6 Statuszeile, Farben und Funktionstasten 8.7 Kommandozeilenoptionen 8.8 Umlaute 8.9 Warnungen IX Die Units 9.1 Funktionen und Prozeduren von tstring.pas 9.2 Funktionen und Prozeduren von tio.pas 9.3 Datentypen, Funktionen und Prozeduren von tmain.pas 9.4 Methoden von TBasic 9.5 Methoden von TItem 9.6 Methoden von TRoom 9.7 Methoden von TLink 9.8 Methoden von TLock 9.9 Methoden von TPlayer 9.10 Methoden von TGame ***************************************************************************** I Einleitung ***************************************************************************** Textopia ist eine Toolbox für die Programmierung klassischer Text-Adventures. Wie andere Systeme auch, die für diese Aufgabe entwickelt wurden, nimmt Textopia Ihnen viel Routinearbeit ab, um die Sie sich kümmern müßten, wenn Sie Ihre Adventures ohne Hilfsmittel in einer Sprache wie C oder Pascal schreiben wollten. Viele Entwicklungssysteme verlangen das Erlernen einer speziellen Programmiersprache und/oder müssen erst an die deutsche Grammatik angepaßt werden. Textopia ermöglicht die Adventureprogrammierung mit Hilfe der vielen Programmierern vertrauten Sprache Pascal und wurde von vornherein für die deutsche Sprache entworfen. Textopia >> arbeitet mit Turbo Pascal unter DOS, mit dem kostenlosen Free Pascal (http://www.tfdec1.fys.kuleuven.ac.be/~michael/fpc/fpc.htm) unter Linux und mit Delphi 4 unter Windows. Mit Textopia geschriebene Spiele müssen daher nur neu kompiliert werden, um sie zwischen DOS, Windows und Linux auszutauschen. >> unterstützt Räume, Türen, Fahrzeuge, Nichtspielercharaktere (NPCs), Behälter, ein- und ausschaltbare Dinge, Lichtquellen, die Unterscheidung zwischen Licht und Dunkelheit, Zeitschalter und Hintergrundprozesse (Timer & Daemons), die Verwaltung mehrerer Spielstände, Automapping, eine Protokolldatei sowie einen Debugmodus. >> verfügt über einen Parser, der 40 vordefinierte Verben und Kommandos beherrscht und sich einfach erweitern läßt. Der Parser unterstützt die Deklination fast aller deutschen Substantive und erkennt Pluralformen, Adjektive, Reflexivpronomen und natürlich Umlaute. >> ahmt das "Look & Feel" klassischer Text-Adventures der 80er Jahre nach. Die Statuszeile, Bildschirmfarben und Funktionstastenbelegung sind nur einige der Dinge, die Sie in Textopia an Ihre eigenen Vorstellungen anpassen können. Dieser Text soll Ihnen als Einstieg in Textopia und als Nachschlagewerk bei der Programmierung eigener Adventures dienen. Grundkenntnisse in Pascal sind zu seinem Verständnis sicher von Vorteil. Am besten lesen Sie diesen Text vor Ihrem Computer, um die Beispielprogramme besser nachvollziehen zu können. Programmiert, getestet und dokumentiert wurde Textopia von Oliver Berse, dem Schreiber dieser Zeilen. Textopia darf selbstverständlich von jedem interessierten Programmierer benutzt und weiterverbreitet werden. Das gilt auch für damit entwickelte Programme. Beachten Sie aber bitte folgende Punkte: (a) Wenn Sie Textopia weiterverbreiten, dann nur mit allen zugehörigen Dateien, auch den Quelltexten. Eigene Programme, die Textopia verwenden, müssen Sie aber nicht im Quelltext weitergeben. (b) Wenn Sie Änderungen (es sollten Verbesserungen sein) an Textopia vornehmen, dürfen Sie natürlich auch Ihren Namen in den Kommentarkopf der Quelltexte setzen. Meinen lassen Sie da bitte stehen. (c) Eine kommerzielle Verwendung, wenn sie denn überhaupt möglich ist, bedarf meiner Zustimmung. Textopia findet sich im IF-Archiv (ftp.gmd.de) und auf der Textopia-Seite meiner Homepage (http://homepage.ruhr-uni-bochum.de/Oliver.Berse/comp1.htm). Dort finden Sie immer die aktuellsten Informationen über Textopia. Berichte über Bugs, Kommentare und Anregungen senden Sie bitte an Oliver.Berse@rz-ruhr-uni-bochum.de. Oliver Berse Juni 1999 ***************************************************************************** II Kompilieren der Units ***************************************************************************** 2.1 Der Quelltext ----------------- Die Quelldateien für die drei Textopia Units sind tstring.pas, tio.pas und tmain.pas. Diese Dateien sind für jedes Textopia-Programm notwendig und müssen zunächst kompiliert werden. Textopia wurde mit Turbo Pascal 6.0 unter DOS, mit Delphi 4 unter Windows 95/98 und mit Free Pascal unter Linux 6.1 programmiert und getestet. Mit Delphi 3 und Windows NT dürfte es auch funktionieren, getestet habe ich das aber nicht. Der Free Pascal Compiler (FPC) unterstützt neben Linux auch OS/2 und das AmigaOS. Mangels entsprechender Soft- und Hardware konnte ich Textopia unter diesen Systemen noch nicht testen. Eine Anpassung der Units an andere TP-kompatible Compiler, wie z.B. das kommerzielle Virtual Pascal, sollte keine unlösbaren Probleme verursachen. Delphi 4 und FPC sind nicht wirklich 100% kompatibel zu Turbo Pascal. Das liegt an der 32-Bit Struktur beider Compiler und der frühen Versionsnummer von FPC, das noch ein paar Kinderkrankheiten hat. Die Quelltexte der Textopia-Units werden aber von allen drei Compilern problemlos geschluckt. Möglich wird dies durch die Compilerdirektive {$DEFINE xxx} am Anfang der Quelltexte. Hier können die Symbole tp, fpc oder delphi definiert werden. An den wenigen kritischen Stellen teilt ein Konstrukt wie {$IFDEF tp} ..code.. {$ELSE} ..code.. {$ENDIF} dem entsprechenden Compiler dann mit, ob er den Code für DOS, Linux oder Windows übersetzen soll. 2.2 Kompilieren mit Turbo Pascal unter DOS ------------------------------------------ Lassen Sie den Compiler mit Build aus der Datei tmain.pas eine Unit-Datei erstellen, die anderen drei Quelldateien werden automatisch mitkompiliert. Von den am Dateianfang gesetzten Compileroptionen sollten die I/O-Kontrolle {$I} und die strenge Stringkontrolle {$V} immer ausgeschaltet, die erweiterte Syntax {$X} immer eingeschaltet bleiben. 2.3 Kompilieren mit Delphi unter Windows ---------------------------------------- Sie können die Units sowohl in der Entwicklungsumgebung (IDE) als auch über die Kommandozeile in einem DOS-Fenster kompilieren. Wenn Sie in der IDE kompilieren, öffnen Sie tmain.pas als Pascal-Projekt, um die benötigten Dateien mitzuübersetzen. Mit der Kommandozeilenversion von Delphi starten Sie die Übersetzung durch Eingabe von dcc32 -cc -b tmain.pas. Die Hinweise und Warnungen, die Delphi beim kompilieren ausgibt, sind nur falscher Alarm und können unberücksichtigt bleiben. Neben den Dateien tmain.pas, tio.pas und tstring.pas übersetzt Delphi auch die Datei dcrt.pas. Diese Unit ersetzt die unter Delphi 4 fehlende Crt-Unit von Turbo Pascal, ohne die Textopia nicht auskommt. DCrt ist jedoch kein vollständiger Ersatz für alle Funktionen der originalen Crt-Unit. Sie simuliert nur die von Textopia verwendeten Routinen und arbeitet in DOS-Fenstern noch recht langsam (zumindest auf meinen 200Mhz Pentium). Wenn Sie Spiele unter Verwendung der Textopia-Units mit Delphi kompilieren, achten Sie darauf, daß sie als Anwendungen für den Textmodus kompiliert werden. Sie können am Anfang des Quelltextes ein {$AppType Console} einfügen, in der IDE unter Projekt/Optionen/Linker den Textmodus einstellen oder den Kommandozeilencompiler mit der Option -cc aufrufen. 2.4 Kompilieren mit Free Pascal unter Linux ------------------------------------------- Geben Sie im Verzeichnis mit den Quelldateien einfach ppc386 -So -B tmain.pas ein, um die Übersetzung zu starten. Mit dem Schalter -So verhält FPC sich bei der Übersetzung kompatibel zu TP6/7, -B entspricht dem Build-Befehl des TP-Compilers. FPC unterstützt mehr Compileroptionen als TP, so können Sie auch für den Prozessortyp Ihres Computers optimierten Code erzeugen. Wenn Sie nur ppc386 eingeben, erhalten Sie eine Liste aller möglichen Optionen. Während der Übersetzung gibt FPC einige Hinweise aus, die Sie allesamt ignorieren können. Meldungen über nicht verwendete Parameter erzeugt FPC fälschlicherweise, wenn eine Prozedur einen Parameter entgegennimmt und ihn ohne Änderungen an eine zweite Prozedur weiterreicht. 2.5 Worauf Sie unter Linux achten sollten ----------------------------------------- Textopia-Programme sind Textanwendungen und funktionieren im Linux-Textmodus oder unter einem Windowmanager in einem Terminalfenster. Letztere kennen meist verschiedene Textmodi, die sich nicht alle mit der Crt-Unit von FPC vertragen. Wenn sich Ihr Terminalfenster im VT100-Modus befindet, wird die von Textopia benutzte Crt-Unit früher oder später einen Segmentation Fault verursachen. Im Console-Modus mit 80x25 oder mehr Zeichen funktioniert Textopia dagegen einwandfrei. Unter dem KDE-Desktop können Sie das K-Terminal oder das in der SuSE Distribution enthaltene Programm Konsole von Lars Doelle verwenden. Bei K-Terminal wählen Sie unter Einstellungen eine Fenstergröße von mindestens 80x52 Zeichen und schalten unter Einstellungen/Terminal den Console-Modus und die Backspace-Taste ein. Im Programm Konsole stellen Sie den 80x25 Zeichen-Modus ein und schalten die Option BS sendet DEL aus. Wenn Sie beim Programmieren unter einem Windowmanager in einem Editorfenster (XEmacs oder was Ihnen sonst so gefällt) den Quelltext bearbeiten und in einem Terminalfenster kompilieren und testen, erhalten Sie eine ähnlich einfach zu bedienende Entwicklungsumgebung wie unter Delphi oder Turbo. ***************************************************************************** III Textopia und OOP ***************************************************************************** 3.1 Einige wichtige Begriffe ---------------------------- Textopia nutzt die Möglichkeiten der objektorientierten Programmierung (OOP) mit Turbo Pascal. Um mit Textopia Adventures zu programmieren, müssen Sie kein Experte in Object Pascal, C++ oder gar Smalltalk sein. Einige Begriffe der OOP sollten Sie aber kennen. Daher habe ich hier, natürlich ohne Anspruch auf Vollständigkeit, einige formale Definitionen aufgeführt. Eine sehr viel genauere "Einführung in objektorientierte Programmierung mit Turbo Pascal" stammt von Joseph Mittendorfer und sei jedem Interessierten zur Lektüre empfohlen. >> Objekt - bildet eine Einheit aus Daten (Variablen) und den sie verarbeitenden Prozeduren und Funktionen, die auch die Methoden des Objekts genannt werden. In Pascal ähnelt ein Objekt einem Record, der auch Prozeduren und Funktionen aufnehmen kann. Ziel der OOP ist es, große Softwareprojekte in mehrere kleine, überschaubare und wiederverwertbare Objekte zu unterteilen. >> Klasse - die Definition der Daten und Methoden eines Objekts. In Turbo Pascal werden Klassen Objekttypen genannt und ähnlich wie Records definiert. Mit der Anweisung TObjekt=OBJECT erzeugen Sie den neuen Objekttyp TObjekt. Wie im Interface-Abschnitt einer Unit, werden in Objektdefinitionen nur die Methodennamen und ihre Parameter aufgeführt, die vollständige Methode wird dann außerhalb der Objektdefinition geschrieben. >> Instanz - Objekt eines Objekttyps. Instanzen und Objekte sind in Pascal Variablen eines Objekttyps. Die Deklaration objekt:TObjekt erzeugt eine Instanz des Objekttyps TObjekt. Wenn Sie einen Zeiger auf einen Objekttyp definieren (pobjekt:=^TObjekt), können Sie Instanzen auch dynamisch mit NEW() erzeugen. >> Vererbung - Eine neue Objektdefinition kann auf einem bereits bestehenden Objekttyp aufbauen. Bei der Objektdefinition wird dazu der Vorfahre in Klammern hinter das Schlüsselwort geschrieben (TNeu=OBJECT(TObjekt)). Der neue Objekttyp erbt dabei alle Variablen und Methoden des Vorfahren und kann diesen neue hinzufügen. Durch Vererbung kann ein Stammbaum voneinander abgeleiteter Objekttypen (die sog. Objekthierarchie) gebildet werden. >> Polymorphismus - von einem Vorfahren geerbte Methoden können in einem neuen Objekttyp überschrieben, d.h. neu definiert werden. Der Name und die Parameterzahl der betreffenden Prozedur oder Funktion bleiben dabei gleich. Verschiedene Aufgaben können so durch Aufruf der gleichen Methode erledigt werden. >> virtuelle Methoden - ermöglichen dem Compiler zur Laufzeit festzustellen, welche der überschriebenen Versionen dieser Methode bei ihrem Aufruf abgearbeitet wird. Virtuelle Methoden werden mit dem nachgestellten Schlüsselwort VIRTUAL gekennzeichnet. >> Konstruktoren - sind Methoden, die die Daten von Instanzen bei ihrer ersten Verwendung initialisieren, d.h. mit bestimmten Werten vorbelegen. In jedem Objekttyp sollte ein Konstruktor definiert werden. In Turbo Pascal erhalten Kontruktoren gewöhnlich den Namen Init. Bei der Erzeugung dynamischer Instanzen kann Init schon bei der Erzeugung mit NEW() aufgerufen werden: NEW(pobjekt,Init). >> Destruktoren - sind Methoden, die dynamische Daten von nicht mehr benutzten Objekten löschen und somit den Speicher aufräumen. In jedem Objekttyp sollte ein Destruktor definiert werden. In Turbo Pascal erhalten Destruktoren gewöhnlich den Namen Done. Lassen Sie sich von diesen Definitionen nicht abschrecken. Sie werden schnell herausbekommen, wie Sie Textopia und OOP für eigene Adventures nutzen können. 3.2 Adventures und Objekte -------------------------- Die Spielwelt fast aller Adventures besteht aus Elementen wie Räumen, simplen Goldmünzen, komplizierten Maschinen und NPCs, die sprechen und sich bewegen können. Neben der Programmierung eines Parsers stellt die Verwaltung dieser Spielwelt das größte Problem bei der Adventureprogrammierung dar. Textopia löst dieses Problem, indem es alle in einem Spiel vorkommenden Dinge als Objekte darstellt. In tmain.pas werden der Objekttyp TBasic und seine drei Nachfolger TRoom, TLink und TItem definiert. Diese Objekttypen stellen die Grundbausteine jedes Textopia-Programms dar: >> TBasic - verwaltet die Informationen, die alle zur Spielwelt gehörenden Objekte gemeinsam haben, u.a. einen Namen. >> TRoom - stellt einen einzelnen Ort in der Spielwelt dar. Dabei kann es sich um einen Raum in einem Gebäude oder um einen Ort unter freiem Himmel handeln. >> TLink - verbindet jeweils zwei Räume und kann in Verbindung mit einem Objekt vom Typ TLock verschiedene Durchgänge und Türen darstellen. >> TItem - besitzt eine Reihe von Attributen, die sich ein- und ausschalten lassen, um die unterschiedlichsten Dinge in einem Adventure zu simulieren. Objekte der Typen TRoom, TLink und TItem werden im folgenden Text auch Räume, Verbindungen und Gegenstände genannt. Aber nicht nur die Spielwelt wird durch Objekte dargestellt. Auch der Spieler (genauer: sein Alter Ego im Spiel) und das Spiel selbst sind Objekte. Spieler und Spiel stellen jeweils ein Objekt der Typen TPlayer und TGame dar: >> TPlayer - verwaltet verschiedene Informationen über den Zustand des Spielers, z.B. seine Position in der Spielwelt und seine Punktzahl. >> TGame - enthält den Parser und verschiedene Methoden zur Kontrolle des Spielverlaufs. Die genannten Objekttypen besitzen zahlreiche virtuelle Methoden, die Sie in abgeleiteten Typen überschreiben können, um ihr Verhalten in einem Spiel an Ihre Vorstellungen anzupassen. Textopia ist weitgehend objektorientiert, besitzt aber auch viele Prozeduren und Funktionen, die nicht zu einem Objekttyp gehören. Wenn Sie den Quelltext durchstöbern, werden Sie zudem feststellen, daß sich einige Sachen noch eine Spur objektorientierter hätten umsetzen lassen. Textopia entwickelte sich aus Experimenten mit einem deutschsprachigen Parser. Daher beansprucht es nicht, ein Lehrbuchbeispiel für OOP zu sein. ***************************************************************************** IV Das erste Programm ***************************************************************************** In den folgenden Kapiteln soll schrittweise ein erstes Textopia-Programm entwickelt werden, das ein Mini-Adventure darstellt. Textopia-Programme sind Pascal-Programme und folgen natürlich deren bekanntem Schema: PROGRAM programmname; USES { Einbinden von Units } CONST { Konstantendeklaration } TYPE { Typendeklaration } VAR { Variablendeklaration } ... { Funktionen und Prozeduren } BEGIN { Hauptprogramm } END. 4.1 Units, Konstanten, Typen und Variablen ------------------------------------------- In jedes Textopia-Programm werden die drei Textopia-Units eingebunden: PROGRAM demo1; USES TString,TIO,TMain; Für jeden Raum, jede Verbindung und für jeden Gegenstand im Programm wird nun eine numerische Konstante benötigt, über die später jedes Objekt eindeutig zu identifizieren ist. Die Konstanten dürfen (eindeutige) Werte zwischen 1 und 65535 annehmen und müssen in keiner Weise geordnet sein. Eine solche Konstante soll im weiteren Text die ID eines Objekts der Spielwelt genannt werden. Für das denkbar einfachste Textopia-Programm, die Simulation eines einzigen leeren Raums (hier ein Wald), können wir also deklarieren: CONST forest_id = 10; In der Typendeklaration können neue Objekttypen definiert werden, um einzelne Methoden neu zu definieren. Mit Ausnahme einiger interner Typen können Sie von allen Objekttypen in Textopia Nachfolger ableiten. Für unser Beispiel können wir aber bei TRoom bleiben und die Typendeklaration überspringen. Alle Objekte werden in Textopia dynamisch angelegt. Im Variablen-Abschnitt müssen wir daher Zeiger auf die benötigten Objekttypen deklarieren, um von ihnen Instanzen erzeugen zu können. Wir können hierbei auf die vordefinierten Zeigertypen PBasic, PRoom, PLink, PItem, PPlayer und PGame zurückgreifen, die einen Zeiger auf die entsprechenden Objekttypen darstellen. Es müssen immer auch zwei Zeiger für das Spieler- und das Spielobjekt (oder für ihre Nachfolger) deklariert werden. Zur Darstellung eines Raums benötigen wir also drei Zeiger: VAR room : PRoom; player : PPlayer; game : PGame; Die im Var-Abschnitt deklarierten Zeiger vom Typ PRoom, PLink, und PItem dienen der Erzeugung aller im Spiel vorkommenden Objekte des entsprechenden Typs. Es wird also nicht für jedes Objekt ein eigener Zeiger benötigt. Wenn Sie später im Spiel einen Zeiger auf ein bestimmtes Objekt benötigen, verwenden Sie die Funktion Adr(_id : WORD) : POINTER Mit einer expliziten Typumwandlung der Form PRoom(Adr(forest_id)) können Sie diesen Pointer in einen Zeiger auf einen Nachfolger von TBasic umwandeln. Verwechseln Sie diese Funktion nicht mit der Pascal-Funktion Addr. Hätten wir Nachfolger von Textopia-Objekten deklariert, müßten wir nun ihre Methoden schreiben. So aber können wir gleich mit dem Hauptprogramm loslegen. 4.2 Vorbereitungen mit Prepare ------------------------------ Auf das BEGIN des Hauptprogramms muß in Textopia immer zuerst die Anweisung Prepare(_filename: STRING; _new: BOOLEAN); folgen. Prepare erfüllt folgende Aufgaben: Zunächst legt es den Dateinamen für die Dateien fest, die im Spielverlauf angelegt werden. Das sind *.sav und *.ttx für die Spielstandverwaltung und *.txt für eine Textdatei, die automatisch das Spielgeschehen protokolliert. Eine Datei *.dat wird nur in der DOS Version angelegt. Sie nimmt die zu den Objekten gehörenden Texte in codierter Form auf. Diese Datei wird nur angelegt, um unter DOS Speicher zu sparen. Unter Windows und Linux bleiben alle Texte im Speicher. Der Parameter _new bestimmt, ob die Datei *.dat bei jedem Programmstart neu angelegt wird, was während der Programmierung sehr zu empfehlen ist, oder eine bereits bestehende Datei verwendet wird. Unter Linux und Windows hat dieser Parameter also keine Bedeutung. Wenn Sie verhindern wollen, daß Spieler die den Objekten zugeordneten Texte (und nur diese) im Programmcode per HexEditor verändern können, setzen Sie _new im fertigen Programm auf False (ein hundertprozentiger Schutz ist das aber auch nicht). Ein Hinweis: Die Werte True und False werden in Textopia häufig verwendet und können mit den bereits deklarierten Konstanten t und f abgekürzt werden. Als nächstes initialisiert Prepare eine interne Liste, die alle von Ihnen definierten Objekte aufnimmt. Dann erzeugt Prepare eine Instanz von PRoom mit der ID 0 und trägt sie als erstes Element in die Liste ein. Dieser Raum 0 dient als einfache Alternative für die Definition und Beseitigung von von Objekten während des Spielverlaufs, die in Textopia nicht vorgesehen ist. Ißt der Spieler beispielsweise einen Keks, wird dieser nicht mehr benötigt und landet im Raum 0. Umgekehrt können Sie aus diesem Raum auch bislang verborgene Objekte hervorzaubern. Daher können wir diesen Raum auch Top (den Zylinder eines Zauberkünstlers) nennen. 4.3 Objekte und ihre Namen: Ein erster Raum ------------------------------------------- Nun aber weiter mit dem Hauptprogramm. Unsere Spielwelt soll zunächst aus nur einem Raum bestehen. Ein Objekt vom Typ TRoom mit der ID room_id erzeugen wir wie folgt: NEW(room,Init(forest_id,'+W%ald#er')); Mit NEW wird eine neue Instanz erzeugt und gleichzeitig der für die Initialisierung ihrer Daten zuständige Konstruktor Init ausgeführt. Die ersten beiden Parameter der Konstruktoren von TRoom, TLink und TItem sind die ID und der Name der neuen Instanz. Über die ID identifizieren Sie als Programmierer eine Instanz und über ihren Namen bezieht der Spieler sich auf ein Objekt in der Spielwelt. Die Sonderzeichen im Namen zeigen Textopia an, wie der Name zu deklinieren ist und welcher Artikel ihm vorgestellt werden muß, wenn er in einer Ausgabe erwähnt wird. Textopia kennt für diesen Zweck folgende Zeichen: - markiert ein feminines Nomen: -Flasche -> die Flasche. + markiert ein maskulines Nomen: +Troll -> der Troll. $ markiert einen Eigennamen: $Tom -> Tom Steht vor dem Namen kein Minus-, Plus- oder Dollarzeichen, so gilt der Name als Neutrum: Fenster -> das Fenster. % markiert vor einem Vokal eine Lautveränderung im Plural: %Apfel -> Äpfel. # markiert den Buchstaben oder die Silbe, die im Plural angehängt wird: Flasche#n -> die Flaschen * markiert ein vorgestelltes Adjektiv. Vor einem Namen darf höchstens ein Adjektiv stehen. Beachten Sie, daß Minus-, Plus- und Dollarzeichen vor dem Adjektiv stehen müssen: +rot* Drache#n -> der rote Drache. Sie können einem Objekt auch mehrere synonyme Namen geben, diese müssen Sie mit einem Semikolon trennen: Raumschiff#e;Raumfahrzeug#e;Sternenschiff#e. Zwischen den Namen dürfen keine Leerzeichen stehen. Hat ein Objekt mehrere Namen, so erwähnt Textopia immer nur den ersten, während der Spieler alle Namen benutzen kann, um sich auf das Objekt zu beziehen. Schließlich können Sie den Namen auch ganz weglassen, was bei Verbindungen oft sinnvoll ist. Der Objektname wird von einem Objekt vom Typ TMemText gespeichert. Wenn p ein Zeiger auf einen Nachfolger von TBasic ist, können Sie mit der Konstruktion p^.name.GetText auf den Objektnamen zugreifen. Für die Deklination von Objektnamen ist die in tstring.pas definierte Stringfunktion Noun() zuständig. Ihr werden folgende Parameter übergeben: >> n1:String - ein Objektname mit seinen Sonderzeichen. >> casus:TCasus - einer der vier grammatischen Fälle, für den der Name dekliniert werden soll. Mögliche Werte sind: nom (Nominativ), acc (Akkusativ), gen (Genitiv) und dat (Dativ). >> def:Boolean - bestimmt, ob der Objektname mit einem bestimmten (True) oder unbestimmten (False) Artikel zurückgegeben wird. >> num:Byte - bestimmt, ob der Objektname im Singular (num=1) oder im Plural (num>1) zurückgegeben wird. Manchmal ist ein Blick in den Duden hilfreich, um mit Noun() das gewünschte Ergebnis zu erzielen. Wenn ein Objektname nur im Nominativ Singular ohne seine Sonderzeichen benötigt wird, kann auch die ebenfalls in tstring.pas definierte Funktion ShortName(str : STRING; mode : BYTE) : STRING verwendet werden. Sie nimmt den Objektnamen mit Sonderzeichen und einen Byte-Parameter entgegen und gibt den Namen ohne Sonderzeichen zurück. Ist der Byte-Parameter gleich 0, wird der Objektname wie bei der Initialisierung angegeben zurückgegeben, bei 1 und 2 wird er in Klein- bzw. Großbuchstaben umgewandelt. 4.4 Statische Objektbeschreibungen und Sonderzeichen ---------------------------------------------------- Wir haben jetzt einen Raum erzeugt und ihm eine ID und einen Namen gegeben. Das reicht natürlich noch nicht. Wir brauchen einen Text, der dem Spieler mitteilt, was in dem Raum los ist, wenn er sich in ihm aufhält. Jedem Objekt eines Nachfolgers von PBasic können wir mit der Methode AddText(str : STRING) eine ausführliche Beschreibung zuordnen. Diese Methode erreichen wir über den Zeiger, mit dem wir das Objekt auch erzeugt haben: WITH room^ DO BEGIN AddText('Fast undurchdringliches Unterholz läßt Sie nur mühsam '+ 'weiterkommen. Weiter westlich scheinen die Bäume weniger '+ 'dicht beieinander zu stehen.\n'); END; Der an AddText() übergebene Text darf maximal 255 Zeichen lang sein. Sie dürfen aber beliebig viele AddText() hintereinander schreiben, womit auch die Länge des Textes nur von Speicher begrenzt wird. Unter DOS werden die mit AddText() hinzugefügten Texte in der Datei *.dat gespeichert. Bei der Textausgabe kümmert sich Textopia selbständig um den Zeilenumbruch. Wird ein zusammengehöriger Text länger als eine Bildschirmseite, wartet Textopia nach der Ausgabe einer Seite automatisch auf einen Tastendruck des Spielers, um den Rest anzuzeigen. Textopia kennt eine Reihe von Steuerzeichen, die Sie im Text einfügen können, um die Textausgabe zu beeinflussen. Eingeleitet werden diese Anweisungen mit dem Zeichen \ (Backlash): \n Erzwingt einen Zeilenumbruch: AddText('Zeile eins\nZeile zwei'); \h Der folgende Text wird mit erhöhter Intensität ausgegeben. Dies entspricht der TP-Anweisung HighVideo. \l Der folgende Text wird mit normaler Intensität ausgegeben. Dies entspricht der TP-Anweisung NormVideo. \r Schaltet die inverse Textdarstellung ein und aus. \b Gibt das folgende Zeichen in Großschrift aus. Einige weitere Steuerzeichen sind für die systemunabhängige Darstellung deutscher Umlaute vorgesehen (s. 8.8). Mit AddText() angegebene Objektbeschreibungen sind statisch. Das heißt, sie lassen sich während des Spiels nicht mehr verändern. Wenn aber z.B. während des Spiels ein Teil der Decke eines Raums einstürzt und eine Tür versperrt, muß natürlich auch die entsprechende Raumbeschreibung geändert werden. Um eine Objektbeschreibung an bestimmte Spielsituationen anpassen zu können, muß ein neuer Objekttyp von einem der Nachfolger von TBasic abgeleitet werden und die Methode TBasic.MyText neu definiert werden. Wird während des Spiels eine Objektbeschreibung ausgegeben, prüft Textopia immer zuerst, ob dem Objekt ein Text mit AddText() hinzugefügt wurde. Ist das nicht der Fall, wird die Methode MyText, die TRoom, TLink und TItem von TBasic erben, aufgerufen. MyText tut gewöhnlich nichts und ist nur definiert, um in einem Enkel von TBasic überschrieben zu werden. In MyText können nun bedingte Objektbeschreibungen erfolgen. Für die Textausgabe muß nicht nur in MyText immer die Prozedur Print(line : STRING) statt Writeln verwendet werden, da sie sich auch um die Bildschirmverwaltung kümmert. In unserem Beispiel bleiben wir aber erstmal bei einer statischen Beschreibung. 4.5 Spieler und Spiel --------------------- Nachdem die Objekte der Spielwelt, hier nur ein Wald, erschaffen sind, müssen wir uns um einige Eigenschaften des Spielers und des eigentlichen Spiels kümmern. Zunächst erzeugen wir eine Instanz von TPlayer (oder eines Nachfolgers davon). Um sie zu initialisieren, müssen wir den entsprechenden Konstruktor mit fünf Parametern aufrufen: Init(_where: WORD; _adress: TAdress; _gender: TGender; _wmin,_wmax: BYTE) >> _where - ist die ID eines Raums oder eines Items, das dem Spieler als Behälter dient (wir kommen darauf zurück), und bestimmt, wo das Spiel beginnt. >> _adress - kann den Wert du, sie oder ihr annehmen und bestimmt die von Textopia benutzte Anrede für den Spieler. In den von Ihnen vorgegeben Texten müssen Sie sich an die gewählte Anrede halten. Entsprechende Personalpronomen und Verbformen können noch nicht automatisch erzeugt werden. >> _gender - bestimmt das Geschlecht von Spielers Alter-Ego und kann während des Spiels mit der PPlayer-Methode ChangeGender gewechselt werden. Mögliche Werte sind natürlich male und female. Dies ist noch eine experimentelle Option, die von Textopia nur bei einer einzigen Ausgabe berücksichtigt wird. Wenn Sie aber ein Spiel im Stil von Infocoms "Leather Goddesses of Phobos" schreiben wollen, ist _gender sicher nützlich. >> _wmin - gibt das Normalgewicht von Spielers Alter-Ego an, wenn er nichts bei sich trägt. >> _wmax - bestimmt das Maximalgewicht des Spielers. Da sich auch allen möglichen Gegenständen ein Gewicht zuordnen läßt, können Sie mit _wmax leicht bestimmen, wieviele Dinge der Spieler mit sich herumtragen darf. Einen Abenteurer können wir also wie folgt in unserem Wald aussetzen: NEW(player,Init(forest_id,sie,male,1,3)); Im letzten Schritt initialisieren wir jetzt das eigentliche Spiel. Der Konstruktor von TGame verlangt folgende Parameter: >> _player:PPlayer - ein Zeiger auf die erzeugte Instanz von TPlayer. >> _statusline:BOOLEAN - bestimmt, ob zu Spielbeginn eine Statuszeile in der ersten Bildschirmzeile angezeigt wird. Die Statuszeile kann auch während des Spiels ein- und ausgeschaltet werden. >> _upsize:BOOLEAN - bestimmt, ob die Spielereingaben in Großbuchstaben erscheinen sollen, um sie optisch von den Spieltexten zu unterscheiden. Textopia unterscheidet aber nicht zwischen Groß- und Kleinschreibung. Intern werden alle Eingaben in Kleinschrift umgewandelt. >> _verbose:BOOLEAN - ist _verbose wahr, so gibt Textopia in jedem Raum, den der Spieler betritt, die vollständige Raumbeschreibung aus, auch wenn der Spieler den Raum bereits besucht hat. Andernfalls werden in bereits bekannten Räumen nur deren Namen ausgegeben. >> _history:BYTE - der Zeileneditor von Textopia erzeugt automatisch eine Liste der letzten n Spielereingaben, die der Spieler mit den Tasten CursorOben/CursorUnten editieren kann. _history bestimmt die Anzahl der darin gespeicherten Eingabezeilen. Unsere Waldszene können wir jetzt mit NEW(game,Init(t,f,t,10)); initialisieren. Bevor wir das Programm starten, können wir mit der Methode AddProlog(_str : STRING) noch Informationen wie den Titel, eine Copyrightmeldung und eine Einleitung oder Vorgeschichte hinzufügen: WITH game^ DO BEGIN AddProlog('Das Zwergengrab\nEine Textopia-Demo von Oliver Berse\n\n'+ 'Drei Tage schon durchstreifen Sie abseits der Straßen den '+ 'alten Wald. Hier irgendwo liegen die mit Schätzen gefüllten '+ 'Gräber der Zwergenkönige, die einst diese Länder '+ 'beherrschten.\n\n'); Der mit AddProlog() dem Spiel hinzugefügte Text erscheint immer zu Spielbeginn. Diese Methode verhält sich ganz ähnlich wie AddText(). Mit der Methode TGame.Run können wir unsere kleine Demonstration jetzt starten. Dabei übernimmt Textopia die Kontrolle über das Spiel, bis der Spieler es beendet. Nach Beendigung des Spiels sollte noch der Destruktor von TGame aufgerufen werden, um belegten Speicherplatz aufzuräumen: Run; Done; END; Vergessen Sie nicht, das Hauptprogramm mit END. abzuschließen. 4.6 Ein erster Dialog --------------------- Wenn sich demo1.pas problemlos kompilieren und starten läßt, sehen Sie jetzt die Statuszeile mit dem Namen des aktuellen Raums sowie den Punkten und Zügen des Spielers. In der unteren Bildschirmhälfte erscheint zuerst der mit AddProlog() zugewiesene Text, die aktuelle Raumbeschreibung und das Zeichen > als Eingabeaufforderung. Haben Sie bei der Initialisierung der Objekte irgendeinen Fehler gemacht, gibt Textopia eine Warnung (s. 8.9) aus und fragt Sie, ob Sie abbrechen oder fortfahren wollen (in einigen besonders harten Fällen wird das Programm auch ohne Rückfrage beendet). In diesem Fall sollten Sie immer abbrechen und die Fehlerursache beseitigen. Setzen Sie das Programm nur fort, wenn Sie genau wissen, daß der gemeldete Fehler keine Katastrophe verursachen kann. Das kleine d vor der Eingabeaufforderung weist darauf hin, daß der Debugmodus aktiv ist. In diesem Modus können Sie einige Debugkommandos eingeben, die Ihnen helfen, das Verhalten Ihrer Objekte zu kontrollieren. Die Debugkommandos werden in 7.6 erläutert. Wenn Ihr Spiel fertig ist und fehlerfrei läuft, sollten Sie den Debugmodus mit der Zeile debug:=f; im Hauptprogramm ausschalten. Viel läßt sich mit nur einem Raum noch nicht anfangen. Ein erster Dialog könnte so aussehen: Das Zwergengrab Eine Textopia-Demo von Oliver Berse Drei Tage schon durchstreifen Sie abseits der Straßen den alten Wald. Hier irgendwo liegen die mit Schätzen gefüllten Gräber der Zwergenkönige, die einst diese Länder beherrschten. WALD Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. d>w Hier geht es nicht weiter. d>#i Sie tragen nichts mit sich 0 Item(s) Gewicht=1 d>warte Die Zeit vergeht... d>#ende Wollen Sie das Spiel wirklich beenden? (j/n) j Die Mit dem Zeichen # beginnenden Kommandos sind sog. Metaverben, die einige Spieloptionen auslösen, das Spielgeschehen selbst aber nicht beeinflussen. Metaverben werden im Abschnitt 7.5 behandelt. Anzahl und Gewicht der Items im Inventar werden nur im Debugmodus angezeigt. ***************************************************************************** V Die Spielwelt ***************************************************************************** 5.1 Gegenstände und Attribute ----------------------------- Bevor wir unserem Programm "Zwergengrab" weitere Räume hinzufügen, geben wir dem Spieler einige nützliche Dinge mit auf den Weg. Mit Ausnahme von Räumen und Türen können mit dem Objekttyp TItem fast alle in einem typischen Adventure vorkommenden Objekte dargestellt werden. Um einen Gegenstand zu erzeugen, brauchen wir zuerst wieder eine Konstante für die ID und einen Zeiger vom Typ PItem. Wenn wir dem Spieler ein Schwert und einen Rucksack mitgeben wollen, können wir unser Programm um folgende Zeilen erweitern (ich führe hier nur die neuen Zeilen auf, die demo1.pas an den entsprechenden Stellen hinzugefügt werden): CONST sword_id = 11; rucksack_id = 12; VAR item : PItem; Der Konstruktor TItem.Init() verlangt zunächst wieder eine ID und einen Namen für das neue Objekt (s. 4.3). Danach müssen folgende vier Parameter angegeben werden: >> _location: PBasic - einen Zeiger auf den Ort, an dem sich das Objekt zu Spielbeginn befinden soll. Dieser Ort kann ein Raum, das Inventar des Spielers oder ein anderer Gegenstand sein. Den benötigten Zeiger liefert die Funktion Adr(). Wenn das Objekt im Inventar des Spielers plaziert werden soll, muß dem Konstruktor der Wert NIL übergeben werden. Während des Spiels können Sie die Position eines Objekt vom Typ PItem mit der Methode MoveItemTo() verändern. Die neue Position wird durch einen Zeiger vom Typ PBasic übergeben, der die gleiche Bedeutung wie _location hat. >> _show: BOOLEAN - bestimmt, ob das Objekt automatisch in der Raumbeschreibung aufgeführt wird. Nach Ausgabe des von Ihnen geschrieben Textes werden bei einer Raumbeschreibung alle im Raum befindlichen Objekte aufgelistet. Wenn Sie das Objekt bereits in Ihrem Text erwähnt haben, können Sie hier False übergeben. Diese Voreinstellung läßt sich im Spiel mit der Methode ListMe() verändern, der True oder False übergeben wird. So können Sie Objekte bis zu einem bestimmten Ereignis vor dem Spieler verstecken. >> _wmin: BYTE - bestimmt das Eigengewicht des Objekts. Wenn Sie das Objekt im Inventar des Spielers plazieren, achten Sie darauf, daß sein Gewicht nicht das Maximalgewicht des Spielers übersteigt. >> _wmax: BYTE - bestimmt das Maximalgewicht des Objekts. Wenn das Objekt andere Objekte aufnehmen kann, läßt sich hiermit bestimmen, wieviele Gegenstände es maximal enthalten darf. Soll das Objekt nicht als Behälter dienen, können Sie hier 0 angeben. Folgende Zeile erzeugt ein Schwert im Inventar des Spielers: NEW(item,Init(sword_id,'Schwert#er',NIL,t,1,0)); Damit wird dem Spiel zwar ein neues Objekt hinzugefügt, aber woher soll das Spiel wissen, was der Spieler damit anfangen kann? Um Textopia mitzuteilen, daß der Spieler den neuen Gegenstand z.B. aufnehmen und weglegen kann, müssen dem Objekt sog. Attribute hinzugefügt werden. Ein Attribut ermöglicht es dem Spieler, mit dem Objekt eine bestimmte Aktionen durchzuführen und ist entweder vorhanden oder nicht vorhanden. Nach seiner Initialisierung besitzt ein Objekt keine Attribute. Mit der Methode TItem.SetAttrib(a : BYTE; _on : BOOLEAN) können Sie abhängig vom Parameter _on ein Attribut hinzufügen (_on=True) oder entfernen (_on=False). Ob ein Objekt über ein bestimmtes Attribut verfügt oder nicht, können Sie im Spiel mit der Methode TItem.Has(a : BYTE) : BOOLEAN prüfen. Zehn Attribute sind bereits als Konstanten deklariert und können beliebig kombiniert werden. Die einzelnen Attribute haben folgende Bedeutung: >> drinkable_at - das Objekt kann getrunken werden. >> edable_at - das Objekt kann gegessen werden. >> enterable_at - das Objekt kann vom Spieler betreten werden. Betreten meint hierbei sowohl hineingehen (Telefonzelle) als auch draufsetzen (Fahrrad). Wenn Sie dieses Attribut setzen, sollten Sie daher mit der Methode TItem.SetPraepos(pp:STRING) auch die entsprechende Präposition, meistens "in" oder "auf", angeben, damit Textopia den Umstand, daß der Spieler ein Objekt betreten hat, korrekt ausdrücken kann. >> moveable_at - der Spieler kann sich mit dem Objekt herumbewegen, wenn er es betreten hat. Ein Objekt mit den Attributen moveable_at und enterable_at kann somit ein Fahrzeug simulieren. >> readable_at - das Objekt kann gelesen werden (Bücher, Schilder etc.) >> shining_at - das Objekt ist eine Lichtquelle. Eine besondere Aktion kann der Spieler hiermit nicht ausführen. Lichtquellen sind aber in ansonsten dunklen Räumen (s. 8.4) sehr praktisch. >> switchable_at - das Objekt kann ein- und ausgeschaltet werden. Nach der Initialisierung ist ein Objekt immer ausgeschaltet. Den aktuellen Schalterzustand können Sie im Spiel mit den Methoden TItem.IsOn und TItem.IsOff überprüfen. Die Methoden TItem.SwitchOn und TItem.SwitchOff verändern den Schalterzustand. >> takeable_at - das Objekt kann vom Spieler weggelegt, aufgenommen und im Inventar mit herumgetragen werden. >> talkable_at - das Objekt kann angeredet werden (NPCs, Mikrophone etc.). Um ein Objekt anzusprechen, muß der Spieler den Objektnamen gefolgt von einem Komma, einem Leerzeichen und einem Text in doppelten Anführungszeichen eingeben. Näheres über NPCs und Dialoge erfahren Sie in 8.1. >> transparent_at - das Objekt ist durchsichtig (aber nicht unsichtbar), was für Behälter bedeutet, daß ihr Inhalt auch im geschlossenen Zustand sichtbar bleibt. Einige Kombinationen der Attribute ergeben sicher sehr surrealistische Objekte. Neben diesen zehn Attributen können Sie auch eigene Attribute deklarieren. Für deren Konstanten können Sie Werte zwischen 10 und 255 wählen. Natürlich müssen Sie dann auch einige Methoden an die neuen Attribute anpassen, lesen Sie also erstmal weiter. Das grade erzeugte Schwert braucht der Spieler nur mit sich herumzutragen: WITH item^ DO BEGIN SetAttrib(takeable_at,t); Jetzt braucht muß noch der Text geschrieben werden, der erscheint, wenn der Spieler näheres über das Schwert erfahren will. Für die Beschreibung von Gegenständen können wir wieder AddText() verwenden: AddText('Das Schwert hat der Waffenschmied Ihnen kurz vor Ihrer '+ 'Abreise gegeben. Es schimmert und ist sehr scharf. Den '+ 'Schmied zu überreden, Ihnen das Schwert anzuschreiben, weil '+ 'Sie die nötigen Taler erst noch in den Gräbern finden '+ 'müßten, war nicht einfach.\n'); END; Wenn Sie einem Gegenstand weder mit AddText() noch mit der Methode MyText eine Beschreibung hinzufügen und der Spieler das Objekt untersucht, gibt Textopia die Meldung "An xxx ist nichts Besonderes zu entdecken" aus. 5.2 Behälter ------------ Nach dem Schwert erzeugen wir noch einen Rucksack: NEW(item,Init(rucksack_id,'+Rucks%ack#e',NIL,t,1,5)); Den Rucksack kann der Spieler wie das Schwert mit sich herumtragen: WITH item^ DO BEGIN SetAttrib(takeable_at,t); END; Zusätzlich soll der Rucksack noch als Behälter dienen. Einen Behälter kann der Spieler öffnen, schließen und abschließen. Natürlich kann er in einen Behälter auch andere Gegenstände hineinlegen (und wieder herausnehmen). Um aus einem Gegenstand einen Behälter zu machen, muß ein Objekt vom Typ TLock erzeugt und dem Gegenstand zugeordnet werden. TLock verwaltet einige Informationen, die alle Objekte benötigen, die sich öffnen und schließen lassen. Daher wird TLock Ihnen auch bei der Erzeugung von Türen wiederbegegnen. Objekte vom Typ TItem und TLink besitzen einen Zeiger namens lock, der auf das ihnen zugeordnete TLock-Objekt zeigt (und der natürlich NIL ist, wenn das entsprechende Objekt kein Behälter und keine Tür ist). Über diesen Zeiger können Sie auf die Methoden von TLock zugreifen. Der Konstruktor von TLock verlangt fünf Parameter: >> _owner: PBasic - einen Zeiger auf ein Objekt vom Typ TItem oder TLink, das als Behälter oder als Tür dienen soll. Einen Zeiger auf das entsprechende Objekt liefert Ihnen während des Spiels die Methode TLock.GetOwner. >> _openable: BOOLEAN - bestimmt, ob der Spieler das Objekt öffnen kann. Im Spiel können Sie diese Voreinstellung mit der Methode TLock.SetOpenable(s:BOOLEAN) ändern. Den aktuellen Zustand dieses Parameters liefert die Methode TLock.IsOpenable. >> _closeable: BOOLEAN - bestimmt, ob der Spieler das Objekt schließen kann. Diese Voreinstellung können Sie im Spiel mit der Methode Tlock.SetCloseable(s:BOOLEAN) ändern. Den aktuellen Zustand dieses Parameters liefert die Methode TLock.IsCloseable. >> _state: TState - Zustand des Objekts bei Spielbeginn, kann die Werte open, closed oder locked annehmen. Den aktuellen Zustand liefert Ihnen während des Spiels die Methode TLock.GetState. Wenn Sie das Schloß einer Verbindung zugeordnet haben, können Sie alternativ auch die Methode TRoom.IsGate() benutzen, die Sie informiert, ob die Verbindung in der übergebenen Richtung offen, geschlossen oder verschlossen ist. >> _key: PItem - einen Zeiger auf einen Gegenstand, der als Schlüssel dient. Nur mit diesem Schlüssel kann der Spieler das Objekt auf- und abschließen. Wird hier NIL angegeben, kann der Spieler das Objekt nur öffnen (wenn es nicht abgeschlossen ist) und schließen, nicht aber abschließen. Der als Schlüssel dienende Gegenstand kann natürlich auch eine Magnetkarte oder Brechstange sein. Im Spiel gelangen Sie über TLock.GetKey wieder an den Schlüssel. Um eine Instanz von TLock zu erzeugen, brauchen wir einen neuen Zeiger. Fügen Sie daher im Var-Abschnitt die Zeile lock : PLock; ein. Mit item wurde der Rucksack erzeugt, dieser Zeiger kann daher gleich dem Konstruktor von TLock übergeben werden: NEW(lock,Init(item,t,t,closed,NIL)); Auf eine besondere Beschreibung des Rucksacks soll einmal verzichtet werden. Wenn Sie während des Spiel auf einen Behälter zugreifen wollen, sollten Sie noch folgende Dinge wissen: Um festzustellen, ob ein Behälter ein bestimmtes Objekt enthält, verwenden Sie die Methode TItem.Contains(x : PItem) : BOOLEAN. Umgekehrt liefert die Methode TItem.GetContainer einen Zeiger vom Typ PItem auf den Behälter, der das Objekt enthält. Objekte vom Typ TItem und TPlayer enthalten ein statisches Objekt namens weight. Dessen Methoden GetCont, GetSum und GetMax liefern die Zahl der im Behälter enthaltenen Objekte, das Gesamtgewicht des Behälters und sein zulässiges Maximalgewicht. Wenn Sie jetzt das Programm demo2.pas starten, können Sie mit unserem Mini-Adventure schon etwas mehr anfangen: >#i Sie tragen folgende Dinge mit sich: Ein Schwert. Einen Rucksack. 2 Item(s) Gewicht=3 >öffne den rucksack Sie öffnen den Rucksack. >lege das schwert in den rucksack Sie legen ein Schwert in den Rucksack. >#i Sie tragen folgende Dinge mit sich: Einen Rucksack, der Rucksack enthält: Ein Schwert. 2 Item(s) Gewicht=3 >nimm das schwert aus dem rucksack Sie nehmen ein Schwert. >#i Sie tragen folgende Dinge mit sich: Ein Schwert. Einen Rucksack. 2 Item(s) Gewicht=3 Wenn Sie meinen, ein Schwert sei zu groß, um in einen Rucksack gelegt zu werden, brauchen Sie nur das Gewicht des Schwerts zu erhöhen, so daß es mehr wiegt als der Rucksack aufnehmen kann. Hierbei werden Sie auch merken, daß Textopia nicht unterscheiden kann, ob ein Objekt von einem Behälter nicht aufgenommen werden kann, weil es zu schwer oder weil es zu groß ist. Für diese Unterscheidung müßte neben dem Gewicht eines Objekts auch noch dessen Größe festgelegt werden (was die Verwaltung der Behälter wesentlich komplexer machen würde). In den meisten Spielen wird die Gewichtsangabe sicher ausreichen. 5.3 Fortbewegungsmittel ----------------------- Eine weitere Gruppe nützlicher Gegenstände sind Fortbewegungsmittel wie Reittiere und Fahrzeuge aller Art. Wenn der Spieler sie betreten hat, bewegen sie sich mit ihm durch die Räume. Geben Sie einem TItem-Objekt, das dem Spieler zur Fortbewegung dienen soll, die Attribute enterable_at und moveable_at. Der Spieler im "Zwergengrab" soll mit einem Pferd durch den Wald reisen können. Wir deklarieren die Konstante horse_id und erzeugen vor der Initialisierung des Spielers ein Pferd: horse_id = 13; NEW(item,Init(horse_id,'Pferd#e',Adr(forest_id),t,1,10); WITH item^ DO BEGIN SetAttrib(enterable_at,t); SetAttrib(moveable_at,t); SetPraepos('auf'); END; Mit SetPraepos() bestimmen Sie, ob der Spieler sich in oder auf einem Fahrzeug befindet (s. 5.1). Um den Spieler zu Spielbeginn gleich aufs Pferd zu setzen, muß im Konstruktor von TPlayer als Startposition jetzt horse_id statt forest_id angegeben werden. Mit demo3.pas lernen Sie den Umgang mit einem Pferd: WALD (auf einem Pferd) Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. >steig vom pferd Ok >schau WALD Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. Ein Pferd. >steig aufs pferd Ok >schau WALD (auf einem Pferd) Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. >us pferd An dem Pferd ist nichts Besonderes zu entdecken. Statt "steig aufs pferd" versteht der Parser natürlich auch "steige auf das pferd". Wie Sie erkennen können, wird ein Fortbewegungsmittel nicht in der Raumbeschreibung erwähnt, wenn der Spieler sich darin oder darauf befindet. Beachten Sie auch, daß der Spieler, wenn er sich in einem betretbaren Objekt befindet, nicht an Objekte herankommt, die sich außerhalb des den Spieler enthaltenen Objekts befinden (In einem Auto oder einer Telefonzelle kann man z.B. schlecht eine auf der Straße liegende Münze aufheben). 5.4 Weitere Räume ----------------- Einen Wald haben wir bereits in 4.3 erzeugt. Für unser Mini-Adventure brauchen wir noch vier weitere Räume: Vom Wald aus soll der Spieler in westlicher Richtung auf eine Lichtung mit einem Hügelgrab gelangen. In dem Hügel befindet sich ein Tor, über eine dahinterliegende Treppe gelangt der Spieler in die unter dem Hügel liegende Grabkammer. Im Südwesten dieser Kammer soll eine weitere Höhle liegen. Im Osten plazieren wir eine Schatzkammer, die durch eine Tür zunächst versperrt wird. Die Karte sieht dann wie folgt aus: Hügelgrab/Tor.....Wald . Treppe nach unten . Grabkammer...Tür/Schatzkammer . Höhle Beginnen wir mit der Deklaration der Konstanten für die neuen Räume: hill_id = 14; grave_id = 15; chamber_id = 16; cave_id = 17; Nach der Initialisierung des Waldes können jetzt die neuen Räume hinzugefügt werden: NEW(room,Init(hill_id,'-Lichtung#en')); NEW(room,Init(grave_id,'-Grabkammer#n')); WITH room^ DO BEGIN AddText('In der Mitte der engen Grabkammer steht ein Sarkophag. In der '+ 'Ostwand befindet sich eine schwere, mit Eisen beschlagene '+ 'Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine '+ 'hinter der Grabkammer liegende Höhle.\n'); END; NEW(room,Init(chamber_id,'-Schatzkammer#n')); WITH room^ DO BEGIN AddText('In dieser Kammer lagerten die Grabbeigaben des Königs.\n'); END; NEW(room,Init(cave_id,'-Höhle#n')); WITH room^ DO BEGIN AddText('Sie befinden sich in der kleinen Höhle im Südwesten der '+ 'Grabkammer. Diese Höhle wurde offensichtlich nicht von den '+ 'Zwergen angelegt, als sie das Grab schufen. Die Wand der '+ 'Grabkammer wurde für die Höhle durchbrochen. Ihre Wände '); AddText('sind mit scheußlichen Symbolen übersät, die nichts mit den '+ 'Schriftzeichen der Zwerge auf dem Sarkophag zu tun haben.\n'); END; Die Lichtung braucht eine variable Beschreibung (s. 4.4), die erwähnen soll, ob das Tor geöffnet oder geschlossen ist. Diese Beschreibung folgt in 5.6. Wenden wir uns zunächst den Dingen in der Grabkammer zu. Der Sarkophag stellt einen Behälter dar. Er enthält einen Knochen und wird von einer unbeweglichen Steinplatte halb verschlossen: sarko_id = 18; stone_id = 19; bone_id = 20; NEW(item,Init(sarko_id,'+Sarkophag',Adr(grave_id),f,1,5)); NEW(lock,Init(item,f,f,open,NIL)); NEW(item,Init(stone_id,'-Steinplatte#n',Adr(grave_id),f,1,0)); NEW(item,Init(bone_id,'+Knochen',Adr(sarko_id),t,1,0)); WITH item^ DO BEGIN SetAttrib(takeable_at,t); AddText('Dies ist der einzige Knochen, der von dem hier bestatteten '+ 'König geblieben ist. Wenn Sie genau hinschauen, entdecken Sie '+ 'an dem Knochen tiefe Kratzer, die nur von sehr scharfen '+ 'Zähnen stammen können.\n'); END; 5.5 Variable Objektbeschreibungen --------------------------------- Der Knochen soll bei der Beschreibung des Sarkophags erwähnt werden. Da der Spieler den Knochen auch herausnehmen kann, sollte der Sarkophag eine variable Raumbeschreibung erhalten. Statt des statischen AddText() muß also die Methode MyText verwendet werden. Hierzu deklarieren wir einen Nachfolger von TItem, der ja auch ein Nachfolger von TBasic ist, und bringen die Beschreibung des Sarkophags in der überschriebenen Methode MyText unter: TYPE TnItem = OBJECT(TItem) PROCEDURE MyText; VIRTUAL; END; PnItem = ^TnItem; Im Var-Abschnitt muß der Typ des Zeigers item in PnItem geändert werden: item : PnItem; Jetzt folgt die Neudefinition von TnItem.MyText, die variable Beschreibung des Sarkophags enthält. Beachten Sie, daß eine überschriebene Methode von allen Instanzen des entsprechenden Objekttyps verwendet werden kann. Daher muß in TnItem.MyText mit der Methode TBasic.GetID : WORD festgestellt werden, für welches Objekt die Beschreibung ausgegeben werden soll: PROCEDURE TnItem.MyText; BEGIN CASE GetID OF sarko_id : BEGIN Print('In der Ihnen unbekannten Zwergensprache ist ein '+ 'Text in die Seiten des Sarkophags gemeißelt. Die '+ 'schwere Steinplatte, die den Sarkophag einmal '+ 'verschloß, ist halb zur Seite geschoben. '); IF Contains(Adr(bone_id)) THEN Print('Im Sarkophag liegt ein einzelner Knochen.\n') ELSE Print('Von dem Zwergenkönig ist im Sarkophag '+ 'nichts mehr zu entdecken.\n'); END; END; END; 5.6 Verbindungen und Türen -------------------------- Die Tür zur Schatzkammer soll sich mit einem Schlüssel öffnen lassen, den der Spieler in der Höhle finden kann. Als Schlüssel dienende Objekte müssen vor der Initialisierung der entsprechnenden Schlösser initialisiert werden: key_id = 21; NEW(item,Init(key_id,'+Schlüssel',Adr(cave_id),t,1,0)); item^.SetAttrib(takeable_at,t); Auf der Karte gibt es vier Verbindungen zwischen den Räumen. Jede von ihnen braucht eine eigene ID. Bei umfangreicheren Karten sollten den Verbindungen aussagekräftigere Namen gegeben werden, hier numerieren wir einfach: link1_id = 22; link2_id = 23; link3_id = 24; link4_id = 25; Im Var-Abschnitt müssen jetzt noch Zeiger vom Typ PLink und PLock deklariert werden: link : PLink; lock : PLock; Der Konstruktor von TLink verlangt zunächst wieder nach einer ID und einem Namen für die Verbindung. Der Name kann entfallen, wenn die Verbindung keine Tür oder ein ähnliches Hindernis darstellen soll. Geben Sie dann einfach einen Leerstring ('') an. Nach ID und Namen müssen noch folgende vier Parameter angegeben werden: >> _from:Word - die ID des ersten Raums. >> _dirto:TDir - Himmelsrichtung (in englisch), in der der Spieler den ersten Raum verlassen kann, um den zweiten zu erreichen. Zusätzlich zu den zehn Himmelsrichtungen (north, northeast usw.) können Sie auch up und down angeben. >> _to:Word - die ID des Raums, den der Spieler aus angegebener Himmelsrichtung vom ersten Raum aus erreicht. >> _show:Boolean - bestimmt, ob die Verbindung automatisch nach der Raumbeschreibung erwähnt wird. Die Zeile NEW(link,Init(link1_id,'',forest_id,west,hill_id,f)); verbindet den Wald in westlicher Richtung mit dem Hügelgrab auf der Lichtung. Die Verbindung zwischen dem Hügel und der Grabkammer wird mit NEW(link,Init(link2_id,'Tor',hill_id,down,grave_id,f)); erzeugt. Damit der Spieler das Tor öffnen und schließen kann, muß dieser Verbindung noch ein Objekt vom Typ TLink zugeordnet werden: NEW(lock,Init(link,t,t,closed,NIL)); Entsprechend werden die übrigen Verbindungen und Türen erzeugt: NEW(link,Init(link3_id,'-Eichentür#en;-Tür#en',grave_id,east,chamber_id,f)); NEW(lock,Init(link,t,t,locked,Adr(key_id))); NEW(link,Init(link4_id,'',grave_id,southwest,cave_id,f)); Es fehlt noch die Beschreibung der Lichtung, die dem Spieler mitteilen soll, ob das Tor im Hügel geöffnet oder geschlossen ist. Hierbei wird ähnlich wie bei der Neudefinition von TItem.MyText vorgegangen, mit dem Unterschied, daß diesmal ein Nachfolger von TRoom benötigt wird. Im Type-Abschnitt wird die Deklaration TnRoom = OBJECT(TRoom) PROCEDURE MyText; VIRTUAL; END; PnRoom = ^TnRoom; hinzugefügt. Im Var-Abschnitt wird der Zeiger room geändert: room : PnRoom; Die variable Raumbeschreibung der Lichtung sieht wie folgt aus: PROCEDURE TnRoom.MyText; BEGIN CASE GetID OF hill_id : BEGIN Print('Ein kleiner Hügel erhebt sich in der Mitte einer '+ 'Lichtung über das Grab eines Zwergenkönigs. '); IF gate[down]^.lock^.GetState=closed THEN Print('Ein steinernes Tor in der Nordseite versperrt '+ 'Ihnen den Weg ins Innere.\n') ELSE Print('Das steinerne Tor in der Nordseite steht '+ 'offen. Dahinter führt eine Treppe in '+ 'das Grab hinab.\n'); END; END; END; Das Array Gate[] ist ein Datenfeld von TRoom und enthält Zeiger auf die Verbindungen in den entsprechenden Himmelsrichtungen. Mit der Abfrage IF gate[south]<>NIL können Sie z.B. feststellen, ob im Süden eine Verbindung zu einem anderen Raum besteht. Ob einer Verbindung ein Schloß zugeordnet ist, teilt Ihnen die Abfrage IF gate[down]^.lock<>NIL mit. Wenn der Verbindung ein Schloß zugeordnet ist können Sie über lock auf die Methoden von TLock zugreifen. Wenn Sie jetzt einmal demo4.pas starten, ist folgender Dialog möglich: WALD (auf einem Pferd) Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. >w LICHTUNG (auf einem Pferd) Ein kleiner Hügel erhebt sich in der Mitte einer Lichtung über das Grab eines Zwergenkönigs. Ein steinernes Tor in der Nordseite versperrt Ihnen den Weg ins Innere. >steig vom pferd Ok >öffne das tor Sie öffnen das Tor. >runter GRABKAMMER In der Mitte der engen Grabkammer steht ein Sarkophag. In der Ostwand befindet sich eine schwere, mit Eisen beschlagene Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine hinter der Grabkammer liegende Höhle. >us sarkophag In der Ihnen unbekannten Zwergensprache ist ein Text in die Seiten des Sarkophags gemeißelt. Die schwere Steinplatte, die den Sarkophag einmal verschloß, ist halb zur Seite geschoben. Im Sarkophag liegt ein einzelner Knochen. >nimm den knochen Sie nehmen einen Knochen. >us knochen Dies ist der einzige Knochen, der von dem hier bestatteten König geblieben ist. Wenn Sie genau hinschauen, entdecken Sie an dem Knochen tiefe Kratzer, die nur von sehr scharfen Zähnen stammen können. >sw HÖHLE Sie befinden sich in der kleinen Höhle im Südwesten der Grabkammer. Diese Höhle wurde offensichtlich nicht von den Zwergen angelegt, als sie das Grab schufen. Die Wand der Grabkammer wurde für die Höhle durchbrochen und die Wände der Höhle sind mit scheußlichen Symbolen übersät, die sich stark von den Schriftzeichen der Zwerge auf dem Sarkophag unterscheiden. Ein Schlüssel. >nimm den schlüssel Sie nehmen einen Schlüssel. >no GRABKAMMER In der Mitte der engen Grabkammer steht ein Sarkophag. In der Ostwand befindet sich eine schwere, mit Eisen beschlagene Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine hinter der Grabkammer liegende Höhle. >öffne die tür mit dem schlüssel Sie öffnen die Eichentür. >o SCHATZKAMMER In dieser Kammer lagerten die Grabbeigaben des Königs. Wenn Sie wollen, können Sie in der Schatzkammer noch verschiedene Schätze wie Münzen, Kronen und Waffen unterbringen. Denken Sie daran, daß mehrfach vorhandene Dinge (z.B. mehrere Goldmünzen) unterschiedliche IDs aber identische Namen bekommen müssen und maximal zehnfach vorhanden sein dürfen. Den Weg des Spielers durch Ihre Räume können Sie mit den Methoden TRoom.FromDir : TDir und TRoom.ToDir : TDir verfolgen. Aus der Richtung FromDir hat der Spieler den Raum betreten, in die Richtung ToDir hat er ihn verlassen. Wenn der Spieler den Raum noch nicht betreten oder verlassen hat, liefern diese Methoden den Wert nowhere. 5.7 Der Scope ------------- In 5.3 haben Sie bereits erfahren, daß der Spieler Objekte nicht erreichen kann, die sich außerhalb des Behälters oder Fahrzeugs befinden, in dem der Spieler sich befindet. Auch in anderen Fällen kann ein Objekt für den Spieler unerreichbar sein. Jeder Nachfolger von TBasic verfügt über die Methode TBasic.Scope : BYTE An ihrem Rückgabewert können Sie erkennen, inwieweit das entsprechende Objekt für den Spieler erreichbar ist. Die einzelnen Werte von Scope sind in tmain.pas als Konstanten mit der Endung *_sd deklariert und haben folgende Bedeutung: >> notinroom_sd - Das Objekt befindet sich nicht mit dem Spieler in einem Raum, es ist daher außer Sicht- und Reichweite. >> visible_sd - Das Objekt ist nur sichtbar, kann vom Spieler aber nicht berührt oder aufgenommen werden. Dies tritt z.B. ein, wenn das Objekt sich in einem verschlossenen, transparenten Behälter befindet. >> reachable_sd - Das Objekt befindet sich mit dem Spieler in einem Raum, aber nicht im Inventar des Spielers oder eines NPC. >> bynpc_sd - Das Objekt wird von einem NPC (s. 8.1) gehalten. >> held_sd - Das Objekt befindet sich im Inventar des Spielers. Die Methode Scope ist in allen Objekttypen unterschiedlich definiert, so kann z.B. der Scope eines Raums niemals held_sd sein. ***************************************************************************** VI Aktionen und Reaktionen ***************************************************************************** 6.1 Ereignisse -------------- Die bisherige Version unseres Mini-Adventures (demo4.pas) läßt dem Spieler eine etwas zu große Bewegungsfreiheit: Nachdem er vom Pferd gestiegen ist und das Tor geöffnet hat, kann er sich wieder auf das Pferd setzen und in die Grabkammer hineinreiten. Wir wollen aber annehmen, daß das Tor und das Grab zu eng und zu klein sind, um sich auf einem Pferd hineinzubegeben. Der Spieler sollte also daran gehindert werden, das Grab zu betreten, wenn er sich noch auf dem Pferd befindet. Um auf Aktionen des Spielers (hier: das Betreten der Grabkammer mit einem Pferd) Einfluß zu nehmen, müssen Sie zunächst wissen, wie diese von Textopia ausgeführt werden. Der Parser versucht, jeder Eingabe des Spielers ein ihm bekanntes Ereignis zuzuordnen. Ein Ereignis tritt z.B. ein, wenn der Spieler ein Objekt untersucht, einen Behälter öffnet oder den Spielstand speichert. Textopia kennt insgesamt 40 Ereignisse, die im Interface-Abschnitt von tmain.pas als Konstanten mit der Endung *_ev deklariert sind. In Abschnitt 7.2 werden Sie erfahren, wie Sie eigene Ereignisse hinzufügen können. Die einzelnen Ereignisse können von einer normalen Spielereingabe, einem Metaverb (s. 7.5) oder einem Debugkommando (s. 7.6) ausgelöst werden und haben folgende Bedeutung: close_ev - Ein Schloß schließen dance_ev - Spieler tanzt dec_ev - Debugkommando .dec drink_ev - Spieler trinkt Objekt eat_ev - Spieler ißt Objekt enter_ev - Spieler betritt Objekt examine_ev - Spieler untersucht Objekt give_ev - Spieler gibt NPC Objekt go_ev - Spieler verläßt Raum inv_ev - Metaverb #i drop_ev - Spieler legt Objekt weg jump_ev - Spieler springt in die Luft kill_ev - Spieler tötet NPC kiss_ev - Spieler küßt NPC leave_ev - Spieler verläßt zuvor betretenes Objekt lista_ev - Debugkommando .browse lists_ev - Debugkommando .list load_ev - Metaverb #laden lock_ev - Schloß abschließen look_ev - Raumbeschreibung map_ev - Metaverb #karte open_ev - Schloß öffnen press_ev - Spieler drückt Objekt quit_ev - Metaverb #ende restart_ev - Metaverb #neu read_ev - Spieler liest Objekt score_ev - Metaverb #punkte save_ev - Metaverb #sichern set1_ev - Metaverb #status set2_ev - Metaverb #beschreibung show_ev - Spieler zeigt NPC Objekt sleep_ev - Spieler schläft smell_ev - Spieler riecht an Objekt switchoff_ev - Spieler schaltet Objekt aus switchon_ev - Spieler schaltet Objekt ein take_ev - Spieler nimmt Objekt auf tell_ev - Spieler spricht zu NPC touch_ev - Spieler berührt Objekt trace_ev - Debugkommando .trace wait_ev - Spieler wartet An einem Ereignis können bis zu zwei Objekte beteiligt sein. Die betroffenen Objekte erwähnt der Spieler meist in der Eingabe: "Lege das Schwert in den Rucksack" oder "Öffne das Tor". Auch Eingaben, in denen der Spieler kein Objekt der Spielwelt ausdrücklich erwähnt, können sich auf ein bestimmtes Objekt beziehen. Das Kommando "schau" bezieht sich z.B. auf den aktuellen Raum und bewirkt die Ausgabe der Beschreibung eines Objekts vom Typ TRoom. Die an einem Ereignis beteiligten Objekte werden vom Parser benachrichtigt und führen die vom Spieler gewünschte Aktion aus. Dabei ruft der Parser die Methode HandleEvent(VAR event : TEvent); VIRTUAL; auf, die alle Nachfolger von TBasic und TGame besitzen. Der Methode wird ein Record vom Typ TEvent übergeben, in der der Parser einige Informationen über die Eingabe speichert. Von den Datenfeldern dieses Records soll zunächst nur das Feld action vom Typ Byte interessieren. In action ist der Wert der dem Ereignis zugeordneten Konstante gespeichert. In der Methode HandleEvent() kann so festgestellt werden welches Ereignis ausgeführt werden soll. Die HandleEvent()-Methoden der einzelnen Objekttypen kümmern sich nur um die Ereignisse, die mit dem jeweiligen Objekttyp auch einen Sinn ergeben. Die von TBasic abgeleiteten Objekttypen leiten ein Ereignis an ihren Vorfahren weiter, wenn sie es nicht selbst abarbeiten. Wenn auch TBasic nicht für das Ereignis zuständig ist, wird die Methode TGame.HandleEvent() aufgerufen. Folgende Tabelle zeigt, welcher Objekttyp für welches Ereignis zuständig ist: TItem TRoom TLink TLock TBasic TGame close_ev go_ev close_ev close_ev dance_ev lista_ev drink_ev look_ev lock_ev lock_ev dec_ev sleep_ev drop_ev open_ev open_ev examine_ev wait_ev eat_ev jump_ev enter_ev lists_ev give_ev touch_ev kill_ev trace_ev kiss_ev leave_ev lock_ev open_ev read_ev show_ev switchoff_ev switchon_ev press_ev take_ev tell_ev TItem und TLink leiten die Ereignisse close_ev, lock_ev und open_ev an das ihnen zugeordnete Objekt vom Typ TLock weiter. Die Ereignisse inv_ev, load_ev, map_ev, quit_ev, restart_ev, save_ev, score_ev, set1_ev und set2_ev gehören zu den Metaverben und werden in Abschnitt 7.4 behandelt. 6.2 BeforeAction und AfterAction -------------------------------- Da jedes Objekt mehrere Ereignisse abarbeiten kann, erfolgt die Verarbeitung der einzelnen Ereignisse in der Methode HandleEvent() immer in einer Case-Anweisung: PROCEDURE Txxx.HandleEvent(VAR event : TEvent); BEGIN WITH event DO BEGIN CASE action OF yyy_ev : { Code... } zzz_ev : { Code... } ELSE vorgaenger_von_Txxx.HandleEvent(event); END; END; END; Die Methode kann auch einen Var-Abschnitt mit lokalen Variablen enthalten. Wenn Sie die Verarbeitung eines einzelnen Ereignisses beeinflussen wollen, müssen Sie nicht die ganze Methode mit allen Ereignissen überschreiben. Der zu einem Ereignis gehörende Codeblock beginnt immer mit dem Aufruf der Methode BeforeAction(VAR event : TEvent) : BOOLEAN; VIRTUAL; BeforeAction() liefert einen Wahrheitswert zurück (normalerweise immer True), von dem die Ausführung der weiteren Anweisungen abhängt. Wenn diese Methode False zurückgibt, wird das betreffende Ereignis nicht weiter ausgeführt. Wenn Sie ein bestimmtes Ereignis beeinflussen wollen, müssen Sie also nur die Methode BeforeAction() des zuständigen Objekttyps überschreiben, in einer Case-Anweisung das Ereignis abfangen und entscheiden, ob die Ausführung in HandleEvent() fortgesetzt werden soll. Sie können so auch den kompletten Code für die Ausführung eines Ereignisses durch eigene Anweisungen ersetzen. In unserem Beispiel müssen wir verhindern, daß der Spieler auf seinem Pferd in das Grab eindringt. Dafür müssen wir das Ereignis go_ev abfangen und prüfen, ob der Spieler auf der Lichtung mit dem Pferd nach unten in den Hügel gehen will. Der Deklaration von TnRoom fügen Sie den Methodenkopf von BeforeAction() hinzu und schreiben diese Methode neu: FUNCTION TnRoom.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN CASE action OF go_ev : IF ToDir=down THEN BEGIN ok:=player^.GetContainer=NIL; IF NOT(ok) THEN Print('Mit dem Pferd können Sie hier nicht hinein.\n'); END; END; END; BeforeAction:=ok; END; Ausprobieren können Sie diese Erweiterung mit dem Programm demo5.pas. Wenn ein Ereignis ausgeführt wurde, gibt HandleEvent() eine entsprechende Mitteilung aus, die den Spieler über die erfolgreiche Ausführung seiner Aktion informiert (z.B. "Sie nehmen x" nach der Eingabe "nimm x"). Wenn Sie diese Meldung verändern oder unterdrücken wollen, müssen Sie die zu BeforeAction() analoge Methode AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; überschreiben, die nach der Ausführung eines Ereignisses und vor Ausgabe einer Meldung aufgerufen wird. Liefert AfterAction() den Wert False zurück, so wird die Meldung unterdrückt. Die in tio.pas deklarierte globale Variable replay vom Typ Boolean wird bei jeder Textausgabe mit Print() auf True gesetzt. An ihr erkennt der Parser, ob während der Ausführung eines Ereignisses bereits ein Text ausgegeben wurde. Führt ein Ereignis nicht zur Ausgabe irgendeines Textes, so gibt der Parser anschließend ein schlichtes "Ok" aus. Manche Ereignisse bestehen nur in der Ausgabe einer Meldung, z.B. bewirkt das Kommando "warte" die Ausgabe "Die Zeit vergeht...", ohne daß irgendwelche weiteren Methoden ausgeführt werden. In solchen Fällen kann AfterAction() die Ausführung eines Ereignisses also verhindern oder ändern. Während der Ausführung eines Ereignisses können Sie auch weitere Ereignisse auslösen, indem Sie die Methode HandleEvent() eines beliebigen Objekts mit der Variable event und einem veränderten Wert für action aufrufen. Textopia tut dies z.B. bei der Ausführung des Ereignisses go_ev in der Methode TRoom.HandleEvent(). Wenn der Spieler mit einem Schlüssel durch eine mit diesem Schlüssel verschlossene Tür gehen will, wird zunächst das Ereignis open_ev ausgelöst. Schauen Sie sich hierzu einmal den Quelltext von TRoom.HandleEvent() an. ***************************************************************************** VII Der Parser ***************************************************************************** 7.1 Die Eingabe --------------- Der Parser (von engl. to parse: einen Satz grammatisch analysieren) ist der wichtigste Bestandteil jedes Text-Adventures. Er nimmt die Spielereingabe entgegen, analysiert sie und sorgt dafür, daß die Befehle des Spielers ausgeführt werden. Eine Schleife in der Methode TGame.Run wartet in jedem Spielzug auf eine Spielereingabe und übergibt sie dem Parser. Die Spielereingabe wird von der in tmain.pas definierten Prozedur Scan(VAR line : STRING; prompt,upsize : BOOLEAN; lmem : BYTE); entgegengenommen. Scan() bildet einen kompletten Zeileneditor, mit dem Sie auch selber Eingaben des Spielers anfordern können. Ihre Parameter haben golgende Bedeutung: >> line - in diesem String wird die Spielereingabe eingetragen. >> prompt - bestimmt, ob das Eingabeaufforderungszeichen > ausgegeben wird. >> upsize - bestimmt, ob die Eingabe in Großbuchstaben erfolgt. Es gilt die in TGame.Init() (s. 4.5) mit dem Parameter _upsize angegebene Voreinstellung. >> lmem - Größe des Zeilenspeichers. Es gilt die in TGame.Init() (s. 4.5) mit dem Parameter _history angegebene Voreinstellung. Die Eingabe kann mit den Tasten [<-], [Einfg], [Pos1], [Ende] und [Esc] editiert werden. Mit den Tasten CursorOben/CursorUnten kann der Zeilenspeicher durchblättert werden. Groß- und Kleinschreibung werden nicht unterschieden. Intern wird die Eingabe in Kleinbuchstaben umgewandelt. Der Eingabestring kann maximal eine Bildschirmzeile lang sein und darf maximal zehn Wörter enthalten, weitere Wörter werden ignoriert. Diese Zahl können Sie mit der in tstring.pas deklarierten Konstante maxbuf erhöhen. Die Satzzeichen ? . und ! können eingegeben werden, werden vom Parser aber ignoriert. Das Komma und das doppelte Anführungszeichen (") haben eine besondere Funktion beim Umgang mit NPCs (s. 8.1) und sollten nur in diesem Zusammenhang verwendet werden. Umlaute können wahlweise auch ausgeschrieben werden. Hat der Spieler sich in der letzten Eingabe mit einem Substantiv auf ein Objekt bezogen, kann er sich mit einem Reflexivpronomen erneut auf das selbe Objekt beziehen: z.B. "lege das Schwert weg" und "nimm es" statt "nimm das Schwert". Für die Bewegung durch die Spielwelt stehen dem Spieler folgende Kommandos zur Verfügung: n, norden, no, nordosten, o, osten, so, südosten, s, süden, sw, südwesten, w, westen, nw, nordwesten. Nach oben und unten gelangt der Spieler mit oben, aufwärts, rauf, hoch, unten, abwärts und runter. Der Parser versteht die Numerale von zwei bis zehn. Mit ihnen können Mengen gleichartiger Dinge angegeben werden. Wenn in einem Raum z.B. fünf Münzen vorhanden sind, kann der Spieler folgende Eingaben machen: "nimm eine Münze", "nimm drei Münzen" oder "nimm alle Münzen". Statt eines Numerals kann der Spieler auch die entsprechende Ziffer eingeben. Für den Parser bislang unverständlich sind Bindewörter (z.B. "und", "oder") und Verneinungen mit "nicht" und "kein". Nach der Eingabe wird der Eingabestring an die Methode TGame.BeforeParsing(VAR input : STRING); VIRTUAL; übergeben. Diese Methode macht normalerweise nichts mit dem String, kann aber überschrieben werden. Wenn der Spieler in ihrem Spiel z.B. von einem Fluch getroffen wird und seinen Orientierungssinn verliert, könnten Sie in dieser Methode die Himmelsrichtungen im Eingabestring vertauschen. 7.2 Verben und Grammatik ------------------------ Der Eingabestring wird von der Methode TGame.Parse() analysiert, die versucht, das zu der Eingabe passende Ereignis zu finden. Die einzelnen Ereignisse werden von verschiedenen Verben ausgelöst. Der Konstruktor von TGame ruft die Methode TGame.ReadLib auf, die eine Liste mit Verben, das sog. Wörterbuch, anlegt. Zusätzlich zu jedem Verb werden im Wörterbuch auch eine zugehörige Grammatik sowie das auszulösende Ereignis gespeichert. Parse() vergleicht nun den Eingabestring mit jedem Eintrag im Wörterbuch. Wenn in der Eingabe ein Verb aus dem Wörterbuch gefunden wird und die Eingabe auch mit der zum Verb gehörenden Grammatik übereinstimmt, wird das entsprechende Ereignis bei den beteiligten Objekten ausgelöst. Das Wörterbuch wird in Readlib durch wiederholten Aufruf der Methode TGame.AddVerb(_verb,_syntax : STRING; _event : BYTE) angelegt. Ihre Parameter haben folgende Bedeutung: >> _verb:String - ein beliebiges Wort, das ein Ereignis auslösen soll. Sie können auch mehrere synonyme Wörter angeben, die durch ein Semikolon getrennt werden müssen. Wenn das Wort ein Metaverb oder ein Debugkommando sein soll, so muß ihm ein Filezeichen (#) bzw. ein Punkt vorangestellt werden. >> _syntax:String - die zum Verb gehörende Grammatik, mit der die Eingabe übereinstimmen muß, um ein Ereignis auszulösen. Es können auch mehrere durch ein Semikolon getrennte Grammatiken angegeben werden. Wenn Sie nur einen Leerstring angeben, genügt die Eingabe des Verbs, um ein Ereignis auszulösen. >> _event:Byte - die Konstante eines Ereignisses. Geben Sie hier eine der in tmain.pas deklarierten Konstanten mit der Endung *_ev an. Was hat es nun mit der Grammatik auf sich? Die Grammatik eines Verbs gibt an, welche Wörter dem Verb im Eingabestring folgen müssen. Sie können sich die Grammatik als eine Schablone vorstellen, in die die Eingabe hineinpassen muß. Eine Grammatik besteht aus einzelnen Token, von denen jedes für ein einzelnes Wort oder eine Wortgruppe steht. Die Token werden in der Grammatik in der Reihenfolge angeordnet, in der die entsprechenden Wörter auch in der Eingabe auftauchen sollen, dabei müssen die einzelnen Token durch ein Pluszeichen voneinander getrennt werden. Textopia kennt sieben verschiedene Token: >> [wort] - ein beliebiges Wort, das nicht Bestandteil eines Objektnamens ist, meistens eine Präposition. Das Wort muß nur in der Grammatik in eckigen Klammern gefaßt sein. Wenn der Parser in der Eingabe auf ein Wort stößt, das weder in einem Objektnamen noch in einer Grammatik auftaucht, gibt er die Meldung "Ich verstehe das Wort 'xxx' nicht" aus. >> held - ein Substantiv, das sich auf ein Objekt im Inventar des Spielers bezieht. Der Scope (s. 5.7) dieses Objekts muß also dem Wert held_sd entsprechen. >> noun - ein Substantiv, das sich auf irgendein beliebiges Objekt in der Spielwelt bezieht. >> npc - ein Substantiv, das sich auf einen NPC bezieht, der sich mit dem Spieler in einem Raum befindet. >> number - ein Numeral zwischen zwei und zehn oder eine Nummer zwischen 0 und 32767. Beachten Sie, daß für Mengenangaben nur Zahlen bis 10 erlaubt sind. Größere Zahlen können natürlich als Telefonnummern oder Geheimcodes verwendet werden. >> reachable - ein Substantiv, das sich auf ein Objekt mit dem Scope reachable_sd bezieht. >> routine - ein Substantiv, das sich auf ein Objekt bezieht, für das die zu überschreibende Methode TGame.MyScope(_id:Word;_action:Byte):BOOLEAN den Wert True liefert. In dieser Methode können Sie bestimmen, ob das Objekt mit der ID _id das Ereignis _action ausführen kann. Eines der Verben, mit dem der Spieler sich auf das Pferd setzen kann, ist z.B. wie folgt definiert: AddVerb('steige;steig','[auf]+reachable;[aufs]+reachable',enter_ev); Wenn Sie sich ReadLib anschauen, werden Sie sehen, daß unterschiedliche Verben gleiche Grammatiken haben und auch das gleiche Ereignis auslösen können. Umgekehrt können gleiche Verben auch zu unterschiedlichen Ereignissen führen, wenn ihre Grammatiken sich unterscheiden. Einige Verben benötigen zwei Substantive (oder können wahlweise mit zwei Substantiven verwendet werden). Um ein Objekt in einen Behälter zu legen, kann der Spieler z.B. "lege das Schwert in den Rucksack" eingeben. Die Definition des entsprechenden Verbs sieht wie folgt aus: AddVerb('lege;wirf;werfe;schmeiß','held+[in]+reachable',drop_ev); Wenn eine Grammatik zwei Substantive enthält, wird das Ereignis grundsätzlich von dem zum ersten Substantiv gehörenden Objekt ausgeführt. Die Eingabe "lege x in y" führt also immer dazu, daß die Methode HandleEvent() des Objekts x das Ereignis drop_ev ausführt und das Objekt in den Behälter y legt. In einigen Fällen können die Substantive x und y jedoch vertauscht werden, ohne daß sich die Bedeutung der Eingabe verändert. Ein Beispiel dafür sind die gleichbedeutenden Eingaben "verschließe die Truhe mit dem Schlüssel" und "verschließe mit dem Schlüssel die Truhe". In diesen Fällen müssen zwei Grammatiken, jeweils eine für eine der beiden möglichen Reihenfolgen, angegeben werden. Wenn in einer Grammatik das zweite Objekt das Ereignis ausführen soll, so muß dem entsprechenden Token das Klammeraffenzeichen (@) vorangestellt werden: AddVerb('verschließe','[mit]+held+@reachable',lock_ev); AddVerb('verschließe','reachable+[mit]+@held',lock_ev); Bevor Sie in Ihrem Programm TGame.Run aufrufen, können Sie dem Wörterbuch mit AddVerb() eigene Verben, Grammatiken und Ereignisse hinzufügen. Ein ganz neues Wörterbuch können Sie anlegen, wenn Sie vor AddVerb() die von ReadLib angelegte Liste mit der Methode TGame.ClearDictionary löschen. Alle Verben eines bestimmten Ereignisses können Sie mit der Methode TGame.DelVerb(_event : BYTE) löschen. In Readlib werden Sie vielleicht die Himmelsrichtungen für die Bewegung (go_ev) vermissen. Diese werden von Parse() unabhängig von der Verbenliste verarbeitet, da es mit dem System des Wörterbuchs nicht möglich ist, zu bestimmen, in welche Richtung der Spieler sich bei dem Ereignis go_ev bewegt. 7.3 TEvent und TNoun -------------------- Wenn der Parser eine Eingabe erkannt hat, übergibt er der Methode HandleEvent() eine Recordvariable vom Typ TEvent (s. 6.1). Dieser Record enthält folgende Informationen: >> action:Byte - der Wert einer Ereigniskonstante. HandleEvent() kann hiermit feststellen, welches Ereignis ausgeführt werden soll. >> exec:Byte - wenn das Ereignis von mehreren gleichartigen Objekten ausgeführt wird (z.B. "nimm drei Münzen"), gibt dieses Feld an, von wievielen Objekten es bereits ausgeführt wurde. >> maxexec:Byte - bestimmt, von wievielen Objekten das Ereignis maximal ausgeführt wird. >> return:Boolean - bestätigt die erfolgreiche Ausführung eines Ereignisses. Diesem Feld wird bei der Ausführung eines Ereignisses in HandleEvent() der Wert True zugewiesen. Wenn ein Ereignis von BeforeAction() unterbrochen wird, genügt eine Textausgabe, um die Variable replay (s. 6.2) auf True zu setzen und dem Parser eine erfolgreiche Ausführung zu signalisieren. >> who:PItem - ist immer gleich NIL, wenn das Ereignis vom Spieler ausgelöst wurde. Ansonsten zeigt who auf das als NPC dienende Objekt (s. 8.1). >> first:PNoun - ein Zeiger auf einen Record vom Typ TNoun, dieser enthält weitere Informationen über das erste Substantiv der Eingabe. >> second:PNoun - ein Zeiger auf einen Record vom Typ TNoun, dieser enthält weitere Informationen über das zweite Substantiv der Eingabe. >> data:Pointer - einen Zeiger auf weitere Daten, die für die Ausführung des Ereignisses benötigt werden. Dieser Zeiger wird nur bei den Ereignissen go_ev und tell_ev (s. 8.1) benötigt, ansonsten ist er gleich NIL. Die Methode TGame.SearchNoun() wird von Parse() aufgerufen und sucht in der Eingabe nach einem Objektnamen. In ihr werden die Recordvariablen vom Typ TNoun ausgefüllt, auf die first und second zeigen. TNoun enthält folgende Informationen über die in der Eingabe vorkommenden Substantive: >> detect:Boolean - das Substantiv konnte einem Objekt zugeordnet werden, wenn dieses Feld True ist, ansonsten wird kein Ereignis ausgeführt. >> adj:Boolean - das Substantiv enthält ein Adjektiv, wenn das Feld True ist. >> number:Integer - eine vom Spieler angegebene Menge. >> xroom:PRoom - ein Zeiger auf ein zum Substantiv gehörendes Objekt vom Typ TRoom. >> xlink:PLink - ein Zeiger auf ein zum Substantiv gehörendes Objekt vom Typ TLink. >> xitem:PItem - ein Zeiger auf ein zum Substantiv gehörendes Objekt vom Typ TItem. Von den Zeigern xroom, xlink und xitem ist immer nur einer ungleich NIL: 7.4 Fehlermeldungen ------------------- Wenn bei der Analyse der Eingabe oder der Ausführung eines Ereignisses ein Problem auftritt, wird die Methode TGame.ParserError(n : BYTE; str : STRING; i : BYTE; VAR error : BOOLEAN); aufgerufen. Mit einer von 37 möglichen Meldungen, informiert diese Methode den Spieler, daß seine Eingabe nicht ausgeführt werden kann. Mögliche Gründe für den Aufruf von ParserError() sind z.B. unbekannte Wörter in der Eingabe, der Versuch des Spielers, mit einem nicht erreichbaren Objekt zu hantieren oder eine bereits abgeschlossene Tür erneut zu verschließen. Die einzelnen Parameter haben folgende Bedeutung: >> n:Byte - ein Fehlercode zwischen 1 und 37. >> str:String - ein Wort aus der Eingabe oder der Name des Objekts, mit dem sich ein Ereignis nicht ausführen läßt. Die Bedeutung dieses Strings hängt vom jeweiligen Fehlercode ab. >> i:Byte - Anzahl der vom fehlgeschlagenen Ereignis betroffenen Objekte. Der entsprechende Objektname wird abhängig von dieser Variable im Singular oder Plural ausgegeben. >> error:Boolean - ist ein Var-Wert und wird von ParserError() auf True gesetzt. Der Aufruf von ParserError() setzt in Parse() und HandleEvent() eine lokale Error-Variable auf True, was die weitere Analyse und Ausführung verhindert. Wenn Sie eine der Meldungen von ParserError() ändern oder weitere Meldungen hinzufügen möchten, müssen Sie diese Methode überschreiben, die gewünschte Meldung abfangen und für andere Fehlercodes wieder TGame.ParserError() aufrufen. Wenn Sie von TGame den Objekttyp TnGame abgeleitet haben, kann das so aussehen: PROCEDURE TnGame.ParserError(n : BYTE; str : STRING; i : BYTE; VAR error : BOOLEAN); BEGIN CASE n OF 5 : Print('Ihre Meldung\n'); ELSE TGame.ParserError(n : BYTE; str : STRING; i : BYTE; VAR error : BOOLEAN); END; END; 7.5 Metaverben -------------- Metaverben beeinflussen einige von der Spielhandlung unabhängige Optionen und verbrauchen keine Spielzeit. Sie beziehen sich nicht auf Objekte in der Spielwelt. Daher werden sie nicht von einer HandleEvent-Methode ausgeführt, sondern führen zum Aufruf verschiedener Methoden von TGame. Bevor der Parser ein Metaverb ausführt, wird die Methode TGame.MetaVerb(mv : BYTE) : BOOLEAN; VIRTUAL; mit dem Wert des entsprechenden Ereignisses aufgerufen, deren Rückgabewert über die weitere Bearbeitung des Ereignisses entscheidet. Wenn Sie MetaVerb() überschreiben, können Sie also die Ausführung von Metaverben beeinflussen. Um eine Verwechslung mit anderen Verben zu verhindern, muß den Metaverben das Filezeichen (#) vorangestellt werden. Folgende Metaverben stehen zur Verfügung: >> #beschreibung - wechselt zwischen ausführlichen und kurzen Beschreibungen bereits bekannter Räumen, ändert also die bei der Initialisierung des Spiels getroffene Voreinstellung. Kann mit #b abgekürzt werden. >> #ende - beendet das Spiel nach einer Sicherheitsabfrage. >> #inventar - zeigt das Inventar des Spielers an. Im Debugmodus werden noch die Anzahl der Objekte im Inventar und das Gesamtgewicht des Spielers angezeigt. Kann mit #inv und #i abgekürzt werden. >> #karte - zeigt eine Karte der bereits bekannten Räume an. Im Debugmodus werden alle Räume angezeigt, egal ob sie bereits besucht wurden. Der aktuelle Raum erscheint dabei immer in der Mitte der Karte, deren Größe von der Zahl der Bildschirmzeilen und Spalten begrenzt wird. >> #laden - lädt einen zuvor gespeicherten Spielstand. Wurden bereits mehrere Spielstände gespeichert, wird ein Menü mit den gespeicherten Spielständen angezeigt, von denen einer ausgewählt werden kann. >> #neu - stellt den Ausgangszustand des Spiels wieder her. >> #punkte - zeigt Punkte- und Zugzahl des Spielers an. Die hierbei aufgerufene Methode TPlayer.RankStr kann überschrieben werden, um zusätzlich zu der Punktzahl einen sich hieraus ergebenden Status des Spielers anzuzeigen. >> #status - schaltet die Statuszeile ein und aus. >> #sichern - speichert einen Spielstand. Nach Eingabe dieses Kommandos wird der Spieler nach einem Namen für den Spielstand gefragt. 7.6 Debugkommandos ------------------ Vier Debugkommandos sollen Ihnen helfen, Überblick über die Objekte der Spielwelt zu behalten. Debugkommandos beeinflussen wie die Metaverben nicht das Spielgeschehen, beziehen sich aber auf einzelne Objekte und werden daher von einer HandleEvent-Methode ausgeführt. Debugkommandos müssen mit einem Punkt beginnen und werden vom Parser nur im Debugmodus (s. 4.6) verstanden. Folgende Debugkommandos stehen zur Verfügung: >> .browse - zeigt die Eigenschaften (Namen, Attribute, Position etc.) einzelner Objekte an. Die Eigenschaften eines Objekts werden von der Methode View ausgegeben, die jeder Nachfolger von TBasic besitzt. Mit den Tasten CursorOben/CursorUnten können Sie alle Objekte anzeigen lassen, mit ESC beenden Sie die Ausgabe. >> .dec - gibt alle Deklinationen des angegebenen Objektnamens aus. Sie können so prüfen, ob Sie die Sonderzeichen für die Deklination (s. 4.4) richtig angegeben haben. >> .list - zeigt die gleichen Informationen wie browse, jedoch nur für das angegebene Objekt. >> .trace - schaltet die Anzeige des Namens und des Scopes eines Objekts an oder aus. Diese Anzeige erfolgt nach jeder Eingabe, so daß die Veränderung des Scopes im Spiel beobachtet werden kann. Wenn Sie die Methode Inspect, die alle Nachfahren von TBasic besitzen, überschreiben, können Sie auch die Veränderung anderer Eigenschaften eines Objekts verfolgen. ***************************************************************************** VIII Verschiedenes ***************************************************************************** 8.1 Nichtspielercharaktere -------------------------- Kehren wir noch einmal zu unserem Mini-Adventure zurück. Damit es dem Spieler in dem Zwergengrab nicht zu langweilig wird, soll ihm ein Troll Gesellschaft leisten. Der Troll soll ein Nichtspielercharakter (kurz NPC) sein. Der Spieler kann mit einem NPC reden und ihm Anweisungen geben, die der NPC ausführt. NPCs werden von Objekten des Typs TItem dargestellt. Um unserem Adventure einen Troll hinzuzufügen, muß demo5.pas also zunächst um die Zeilen troll_id = 26; NEW(item,Init(troll_id,'+Troll#e',Adr(cave_id),t,1,5)); erweitert werden. Jedes Objekt vom Typ TItem ist entweder unbelebt, lebendig oder tot. Als NPCs dienende Objekte sind lebendig oder tot, alle anderen Objekte vom Typ TItem sind immer unbelebt. Diesen Zuständen entsprechen die Ausprägungen des in tmain.pas deklarierten Variablentyps TMatter: inanimate, dead und alive. Jedes Objekt wird automatisch mit dem Zustand inanimate initialisiert. Mit der Methode TItem.SetMatter(state : Tmatter) läßt sich der Zustand eines Objekts ändern, mit TItem.GetMatter : TMatter läßt er sich abfragen. Ein "lebendes" Objekt kann nicht automatisch angesprochen werden. Damit der Spieler den Troll auch anreden kann, muß noch das Attribut talkable_at gesetzt werden. Folgende Zeilen machen aus dem eben erzeugten Objekt einen NPC: WITH item^ DO BEGIN SetMatter(alive); SetAttrib(talkable_at); AddText('Berichte über Trolle mit ihren langen, scharfen Fangzähnen '+ 'und Klauen haben Sie bisher nur in Tavernen gehört und nie '+ 'ernst genommen. Jetzt sollten Sie Ihre Meinung ändern.\n'); END; Um einen NPC anzusprechen, muß der Spieler dessen Namen gefolgt von einem Komma, einem Leerzeichen und einem Text in doppelten Anführungszeichen eingeben. Starten Sie jetzt einmal demo6.pas und gehen in die Höhle: HÖHLE Sie befinden sich in der kleinen Höhle im Südwesten der Grabkammer. Diese Höhle wurde offensichtlich nicht von den Zwergen angelegt, als sie das Grab schufen. Die Wand der Grabkammer wurde für die Höhle durchbrochen und die Wände der Höhle sind mit scheußlichen Symbolen übersät, die sich stark von den Schriftzeichen der Zwerge auf dem Sarkophag unterscheiden. Ein Schlüssel. Ein Troll. >troll, "wer sind sie?" Der Troll hört Ihnen nicht zu. Die Meldung "x hört Ihnen nicht zu" ist die Standardantwort auf jede Anrede. Um eine sinnvollere Reaktion des NPC hervorzurufen, müssen Sie die Methode TItem.BeforeAction() überschreiben und das Ereignis tell_ev abfangen. Den Objekttyp TnItem haben wir bereits von TItem abgeleitet, so daß wir seiner Deklaration nur noch die Zeile FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; hinzufügen müssen, um folgende Reaktion des Trolls auf eine Anrede bestimmen zu können: FUNCTION TnItem.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN CASE action OF tell_ev : BEGIN ok:=f; IF POS('zwerg',STRING(data^))>0 THEN Print('Der Troll sagt: "Die Zwergenknochen sind mir '+ 'ausgegangen, du kommst genau richtig..."\n') ELSE Print('Der Troll leckt nur seine Zähne und '+ 'schaut Sie mit einem sehr beunruhigenden '+ 'Blick an.\n'); END; END; END; BeforeAction:=ok; END; Um den String mit der wörtlichen Rede des Spielers zu erreichen, müssen Sie die Pointer-Variable data des Records event in einen String umwandeln. In unserem Fall hängt die Antwort des Trolls davon ab, ob in der Anrede des Spielers der Teilstring "zwerg" auftaucht. Diese Typumwandlung funktioniert nur bei dem Ereignis tell_ev. Beachten Sie, daß wenn in Ihrem Spiel mehrere NPCs auftauchen, Sie erst mit der Methode GetID feststellen müssen, welcher NPC angesprochen wurde. Bevor Sie den Spieler mit seinem Schwert den Troll meucheln lassen, schauen Sie sich noch an, wie NPCs Befehle ausführen können. Um einem NPC ein Kommando zu geben, muß der Spieler nur den Namen des NPC gefolgt von einem Komma, einem Leerzeichen und der gewünschten Anweisung eingeben: HÖHLE Sie befinden sich in der kleinen Höhle im Südwesten der Grabkammer. Diese Höhle wurde offensichtlich nicht von den Zwergen angelegt, als sie das Grab schufen. Die Wand der Grabkammer wurde für die Höhle durchbrochen und die Wände der Höhle sind mit scheußlichen Symbolen übersät, die sich stark von den Schriftzeichen der Zwerge auf dem Sarkophag unterscheiden. Ein Troll. Ein Schlüssel. >troll, nimm den schlüssel Der Troll nimmt einen Schlüssel. >troll, gib mir den schlüssel Der Troll reicht Ihnen einen Schlüssel. >gib den schlüssel dem troll Sie geben dem Troll einen Schlüssel. >troll, lege den schlüssel weg Der Troll legt einen Schlüssel weg. Der Variable who vom Typ PItem im Record event können Sie entnehmen, ob ein Ereignis vom Spieler oder von einem NPC ausgelöst wurde. Wurde ein Ereignis vom Spieler ausgelöst, ist dieser Zeiger gleich NIL, ansonsten zeigt er auf den handelnden NPC. Wenn der Troll für die Anweisungen des Spielers taub sein soll, können Sie die obige Methode wie folgt erweitern: FUNCTION TnItem.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN IF who<>NIL THEN BEGIN ok:=f; Print('Der Troll denkt nicht daran, Ihre Befehle auszuführen.\n'); END; CASE action OF ... END; END; BeforeAction:=ok; END; Der Versuch, den Troll zu erschlagen, führt zu der wenig befriedigenden Meldung "Sie sind ein friedliebender Spieler" (bzw. "Spielerin", dies ist die einzige Stelle, an der Textopia das Spielergeschlecht berücksichtigt). Das ist natürlich nur eine Standardantwort, die Sie in BeforeAction() ändern können. Um es dem Spieler nicht zu einfach zu machen, tötet nur jeder zweite Schwertstreich den Troll: FUNCTION TnItem.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN IF who<>NIL THEN BEGIN ... END; CASE action OF tell_ev : BEGIN ... END; kill_ev : BEGIN ok:=f; IF RANDOM(2)=0 THEN BEGIN SetMatter(dead); Print('Mit einem schrecklichen Schrei stirbt der '+ 'Troll, als Ihr Schwert ihn trifft.\n'); player^.IncScores(5); END ELSE Print('Sie haben noch nie ein Schwert in der '+ 'Hand gehabt, stimmts?\n'); END; END; END; BeforeAction:=ok; END; Der Spieler kann jetzt den Troll töten, einen Schlüssel finden und die Schatzkammer ausräumen (wenn Sie da Schätze reinlegen). Für die Bewältigung bestimmter Aufgaben können Sie dem Spieler Punkte geben. Benutzen Sie hierfür die Methode TPlayer.IncScores(ds : WORD) Mit der Methode TPlayer.GetScores : WORD können Sie die aktuelle Punktzahl abfragen. Das Ereignis kill_ev können Sie mit dem Programm demo7.pas ausprobieren. 8.2 Leben, Tod und Sieg des Spielers ------------------------------------ In dem Mini-Adventure können Sie dem Troll auch die Chance geben, den Spieler zu töten. Wenn es Ihnen gefällt, können Sie das Alter-Ego des Spielers natürlich auch auf andere Weise umkommen lassen. Der Spieler verfügt über einen Status, der die Werte der Konstanten alive_ps, dead_ps und victory_ps annehmen kann. Bei Spielbeginn bekommt der Spielerstatus automatisch den Wert alive_ps zugeordnet. Der Dialog zwischen Spieler und Parser endet, wenn der Spielerstatus den Wert dead_ps oder victory_ps annimmt. Den Spielerstatus können Sie mit der Methode TPlayer.SetState(s : BYTE) ändern. Töten Sie den Spieler mit SetState(dead_ps), so wird die Methode TPlayer.AfterLife aufgerufen, die normalerweise die erreichte Punktzahl ausgibt. Hat der Spieler hingegen das finale Rätsel gelöst oder eine bestimmte Punktzahl erreicht, können Sie das Spiel mit SetState(victory_ps) beenden. Hiernach wird die Methode TPlayer.Victory aufgerufen, die normalerweise ebenfalls die erreichte Punktzahl ausgibt. Beide Methoden können Sie überschreiben, um einen Nachruf oder Glückwünsche auszugeben. Nach Beendigung dieser Methoden kann der Spieler das Spiel erneut beginnen, einen gespeicherten Spielstand laden oder das Programm ganz beenden. 8.3 Hintergrundprozesse, Zeitschalter und Spielzeit --------------------------------------------------- Ein Hintergrundprozeß, auch Dämon genannt, ist eine Methode mit beliebigem Programmcode, die jedem Objekt vom Typ TRoom, TItem und TLink zugeordnet werden kann und nach jeder Spielrunde einmal ausgeführt wird. Ein Hintergrundprozeß wird mit der Methode StartDaemon gestartet. Die Methode StopDaemon schaltet ihn wieder aus. Ist ein solcher Dämon einmal aktiv, wird nach jeder Spielrunde die Methode RunDaemon; VIRTUAL; aufgerufen, die Sie überschreiben müssen, damit sie etwas tut. In dieser Methode können Sie z.B. NPCs von Raum zu Raum bewegen (s. 5.1) oder überprüfen, ob eine bestimmte Spielsituation eingetreten ist, auf die irgendein Objekt reagieren soll. Im Gegensatz zu Hintergrundprozessen führen Zeitschalter nicht nach jeder Spielrunde zur Ausführung einer Routine, sondern erst nach Ablauf einer bestimmten Anzahl von Runden. Gestartet wird ein Zeitschalter (oder Timer) mit der Methode StartTimer(rounds : BYTE) Ihr müssen Sie die Zahl der Runden übergeben, nach deren Ablauf die Methode TimeOut; VIRTUAL; aufgerufen wird. TimeOut müssen Sie überschreiben, um zu bestimmen, was nach der angegeben Rundenzahl geschehen soll. Einen Timer können Sie mit StopTimer auch vorzeitig abschalten (wenn es dem Spieler z.B. gelingt, eine Zeitbombe zu entschärfen). Zeitschalter und Dämon eines Objekts dürfen auch gleichzeitig aktiv sein, beachten Sie dann, daß RunDaemon immer vor TimeOut ausgeführt wird. Textopia verfolgt auch die Spielzeit (die nichts mit der Systemzeit Ihres Computers zu tun hat), die Ihre Dämonen und Zeitschalter auch berücksichtigen können. Die Spielzeit wird mit einer 24 Stunden Uhr ab Spielbeginn gemessen und kann mit der Methode TGame.GetTime(VAR hour,min : BYTE) ermittelt werden. Die Spielzeit wird zu Spielbeginn auf 0:00 Uhr gesetzt. Die Uhrzeit können Sie zu Spielbeginn oder irgendwann im Spiel mit der Methode TGame.SetTime(_time,_rate : WORD) ändern. Der Parameter _time gibt die Uhrzeit als Zahl der seit Mitternacht vergangenen Minuten an und berechnet sich nach der Formel 60*Stunden+Minuten. Der Parameter _rate gibt an, wieviele Minuten während einer Runde vergehen. Mit einem Dämon können wir das Mini-Adventure noch mal erweitern. Bislang steht der Troll nur regungslos in der Höhle herum, jetzt soll er die Initiative ergreifen, sobald der Spieler die Höhle betritt. Der Spieler wird in der Höhle vom Troll angegriffen und getötet, wenn er flüchten will. Erschlägt der Spieler den Troll, so findet er den Schlüssel, der dem toten Troll aus dem Wams fällt. Zunächst muß statt der statischen Beschreibung der Höhle eine variable Beschreibung in TnRoom.MyText erfolgen. Hier wird festgestellt, ob der Troll noch lebt. Ist er tot, wird die normale Beschreibung der Höhle ausgegeben. Lebt der Troll noch, so wird dem Spieler der Angriff mitgeteilt und der Dämon des Trolls gestartet, um den Troll nun jede Runde den Spieler angreifen zu lassen. In TnRoom.BeforeAction() wird der Spieler an der Flucht gehindert und getötet. Wenn der Spieler den Troll erschlägt, was nur mit dem Schwert möglich ist, wird der Dämon des Trolls abgeschaltet und der Schlüssel in die Höhle gelegt. Der Deklaration von TnItem wird die Zeile PROCEDURE RunDaemon; VIRTUAL; hinzugefügt. Definiert wird die Methode wie folgt: PROCEDURE TnItem.RunDaemon; BEGIN CASE RANDOM(3) OF 0 : Print('Die furchtbaren Klauen des Trolls verfehlen Sie um '+ 'Haaresbreite.\n'); 1 : Print('Der Troll schlägt Ihnen beinahe das Schwert aus der '+ 'Hand.\n'); 2 : Print('Sie können grade noch dem Schlag des Trolls ausweichen.\n'); END; END; Echten Schaden fügt der Troll dem Spieler also nur zu, wenn dieser zu flüchten versucht. Auf eine Prüfung, ob der Spieler tatsächlich ein Schwert hält, soll bei der zweiten Ausgabe einmal verzichtet werden. Beachten Sie, daß Sie in RunDaemon mit GetID feststellen müssen, welcher Dämon grade ausgeführt wird, wenn mehrere Hintergrundprozesse aktiv sind. Eingeschaltet wird der Dämon des Trolls in TnRoom.MyText, wenn der Spieler die Höhle zum ersten Mal betritt: PROCEDURE TnRoom.MyText; BEGIN CASE GetID OF hill_id : BEGIN ... END; cave_id : IF PItem(Adr(troll_id))^.GetMatter=alive THEN BEGIN Print('Als Sie die Höhle betreten springt Ihnen mit '+ 'markerschütterndem Gebrüll ein Troll entgegen!\n'); PItem(Adr(troll_id))^.StartDaemon; END ELSE BEGIN Print('Sie befinden sich in der kleinen Höhle im '+ 'Südwesten der Grabkammer. Diese Höhle '+ 'wurde offensichtlich nicht von den Zwergen '+ 'angelegt, als sie das Grab schufen. Die '+ 'Wand der Grabkammer wurde für die Höhle '+ 'durchbrochen und die Wände '); Print('der Höhle sind mit scheußlichen Symbolen '+ 'übersät, die sich stark von den '+ 'Schriftzeichen der Zwerge auf dem '+ 'Sarkophag unterscheiden.\n'); END; END; END; Bei der Initialisierung des Troll wurde der Parameter show auf False gesetzt, damit der Troll nach dem ersten Angriff nicht noch mal erwähnt wird. Erst wenn er stirbt, wird seine Leiche mit ListMe(True) wieder in die automatische Raumbeschreibung aufgenommen. Wenn der Dämon nun aktiv ist, prüfen wir, ob der Spieler die Höhle verlassen will und der Troll noch lebt: FUNCTION TnRoom.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN CASE action OF go_ev : IF ToDir=down THEN BEGIN ... END ELSE IF (ToDir=northeast) AND (PItem(Adr(troll_id))^.GetMatter=alive) THEN BEGIN Print('Sie spüren kurz eine Klaue im Nacken, '+ 'dann wird es um Sie dunkel...\n'); player^.SetState(dead_ps); END; END; END; BeforeAction:=ok; END; Schließlich soll der Spieler den Troll mit dem Schwert töten, dessen Dämon damit ausschalten und einen Schlüssel finden: FUNCTION TnItem.BeforeAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN IF who<>NIL THEN BEGIN ... END; CASE action OF tell_ev : BEGIN ... END; kill_ev : BEGIN ok:=f; IF PItem(Adr(sword_id))^.GetLocation=NIL THEN BEGIN IF RANDOM(2)=0 THEN BEGIN SetMatter(dead); ListMe(t); StopDaemon; Print('Mit einem schrecklichen Schrei stirbt der '+ 'Troll, als Ihr Schwert ihn trifft. Als er zu '+ 'Boden stürzt, fällt ein Schlüssel aus seinem '+ 'Wams.\n'); PItem(Adr(key_id))^.MoveItemTo(Adr(cave_id)); player^.IncScores(5); END ELSE Print('Sie haben noch nie ein Schwert in der '+ 'Hand gehabt, stimmts?\n'); END ELSE Print('Mit bloßen Händen??\n'); END; END; END; BeforeAction:=ok; END; 8.4 Licht und Dunkel -------------------- Räume werden von Textopia automatisch beleuchtet, so daß der Spieler in ihnen immer sehen kann. Mit der Methode TRoom.SetLight(_light : BOOLEAN) können Sie die automatische Beleuchtung aus- und wieder einschalten. Mit der Methode TRoom.HasLight : BOOLEAN können Sie feststellen, ob der Spieler in einem Raum sehen kann. Wenn der Spieler ein Item mit dem Attribut shining_at bei sich trägt, kann er in jedem Raum sehen. Befindet der Spieler sich in einem dunklen Raum, so wird in der Statuszeile statt dem Raumnamen das Wort "Finsternis" ausgegeben und die Raumbeschreibung erfolgt über die Methode TRoom.MyDarkness, die Sie überschreiben müssen, um dem Spieler mitzuteilen, was bzw. was er nicht sehen kann. Bis auf das Kommando "schau" bleiben alle Aktionen des Spielers von der Dunkelheit unbeeinflußt, solange Sie das fehlende Licht nicht in irgendeiner überschriebenen Methode berücksichtigen. In der letzten Version unseres Mini-Adventures (demo9.pas) statten wir den Spieler jetzt mit einer Fackel aus und schalten das Licht im Grab ab, wenn das Tor von innen geschlossen wird. Zunächst wird die Fackel erzeugt, dazu wird im Abschnitt Const die Zeile torch_id = 27; hinzugefügt. Da zum Anzünden und Löschen der Fackel zwei neue Verben und Ereignisse gebraucht werden, können hier auch gleich die neuen Ereigniskonstanten deklariert werden: fireon_ev = 140; fireoff_ev = 141; Jetzt initialisieren wir eine nicht brennende Fackel und legen Sie in den Rucksack: NEW(item,Init(torch_id,'-Fackel#n',Adr(rucksack_id),t,1,0)); item^.SetAttrib(takeable_at,t); Nach der Initialisierung des Spiels müssen die neuen Verben "anzünden" und "löschen" dem Wörterbuch bekanntgemacht werden: AddVerb('zünde;zünd','held+[an]',fireon_ev); AddVerb('lösch;lösche;mache;mach','held;held+[aus]',fireoff_ev); Die neuen Verben werden von TItem.HandleEvent() nicht berücksichtigt und können daher nicht in TnItem.BeforeAction() abgehandelt werden. Daher überschreiben wir HandleEvent() für die neuen Ereignisse und geben via Else alle anderen Ereignisse an den Vorgänger weiter. Nachdem die Zeile PROCEDURE TnItem.HandleEvent(VAR event : TEvent); VIRTUAL; der Definition von TnItem hinzugefügt wurde, kann jetzt der Code für die Fackel geschrieben werden: PROCEDURE TnItem.HandleEvent(VAR event : TEvent); BEGIN WITH event DO BEGIN CASE action OF fireon_ev : IF GetID=torch_id THEN BEGIN IF NOT(Has(shining_at)) THEN BEGIN IF player^.GetLocation^.GetID IN [grave_id,cave_id,chamber_id] THEN BEGIN PnRoom(Adr(grave_id))^.SetLight(t); PnRoom(Adr(cave_id))^.SetLight(t); PnRoom(Adr(chamber_id))^.SetLight(t); END; SetAttrib(shining_at,t); return:=t; END ELSE Print('Die Fackel brennt bereits.\n'); END ELSE Print('Das geht nicht.\n'); fireoff_ev : IF GetID=torch_id THEN BEGIN IF Has(shining_at) THEN BEGIN IF (player^.GetLocation^.GetID IN [grave_id,cave_id,chamber_id]) AND (PnRoom(Adr(grave_id))^.IsGate(up)=closed) THEN BEGIN PnRoom(Adr(grave_id))^.SetLight(f); PnRoom(Adr(cave_id))^.SetLight(f); PnRoom(Adr(chamber_id))^.SetLight(f); END; SetAttrib(shining_at,f); return:=t; END ELSE Print('Die Fackel ist bereits aus.\n'); END ELSE Print('Das geht nicht.\n'); ELSE TItem.HandleEvent(event); END; END; END; Bevor die Fackel ein- und ausgeschaltet wird, wird ihr aktueller Status und die Position des Spielers überprüft, dieser muß sich irgendwo im Grab befinden. Wenn die Fackel gelöscht wird, wird die Beleuchtung nur abgeschaltet, wenn das Tor geschlossen ist und kein Sonnenlicht ins Grab dringt. Die auf True gesetzte Variable return teilt dem Parser die erfolgreiche Ausführung der neuen Ereignisse mit. Die Ereignisse open_ev und close_ev werden von TLock behandelt. Hiervon wird ein Nachfolger benötigt, um die Beleuchtung im Grab ein- und auszuschalten, wenn der Spieler das Tor öffnet und schließt. Überschrieben werden müssen die Methoden BeforeAction() und AfterAction(): TnLock = OBJECT(TLock) FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; END; PnLock = ^TnLock; Entsprechend muß der generische Zeiger lock nun vom Typ PnLock sein: lock : PnLock; Das Tor wird von TnLock wie folgt behandelt: FUNCTION TnLock.BeforeAction(event : TEvent) : BOOLEAN; BEGIN WITH event DO BEGIN CASE action OF close_ev : IF (GetOwner=Adr(link2_id)) AND (player^.GetLocation=Adr(grave_id)) AND (NOT(PItem(Adr(torch_id))^.Has(shining_at))) THEN BEGIN PnRoom(Adr(grave_id))^.SetLight(f); PnRoom(Adr(cave_id))^.SetLight(f); PnRoom(Adr(chamber_id))^.SetLight(f); Print('Es wird dunkel im Grab...\n'); END; open_ev : IF (GetOwner=Adr(link2_id)) AND (player^.GetLocation=Adr(grave_id)) THEN BEGIN PnRoom(Adr(grave_id))^.SetLight(t); PnRoom(Adr(cave_id))^.SetLight(t); PnRoom(Adr(chamber_id))^.SetLight(t); Print('Sonnenlich strömt wieder ins Grab.\n'); END; END; END; BeforeAction:=t; END; Da Schlösser keine ID besitzen, muß hier mit GetOwner geprüft werden, ob es sich um das der Torverbindung zugeordnete Schloß handelt. In BeforeAction() wird bereits ein Text ausgegeben, wenn der Spieler den Zustand des Tors verändert, daher sollte AfterAction() weitere Meldungen des Parsers ("Sie öffnen/schließen das Tor") verhindern: FUNCTION TnLock.AfterAction(event : TEvent) : BOOLEAN; VAR ok : BOOLEAN; BEGIN ok:=t; WITH event DO BEGIN CASE action OF close_ev : IF (GetOwner=Adr(link2_id)) AND (player^.GetLocation=Adr(grave_id)) THEN ok:=f; open_ev : IF (GetOwner=Adr(link2_id)) AND (player^.GetLocation=Adr(grave_id)) THEN ok:=f; END; END; AfterAction:=ok; END; Schließlich muß noch TnRoom.MyDarkness überschrieben werden. Fügen Sie den Methodenkopf in die Definition von TnRoom ein und schreiben Sie etwas wie PROCEDURE TnRoom.MyDarkness; BEGIN Print('Sie können hier kaum etwas sehen...\n'); END; Wenn Sie jetzt demo9.pas starten, sollte der folgende Dialog möglich sein: WALD (auf einem Pferd) Fast undurchdringliches Unterholz läßt Sie nur mühsam weiterkommen. Weiter westlich scheinen die Bäume weniger dicht beieinander zu stehen. >w LICHTUNG (auf einem Pferd) Ein kleiner Hügel erhebt sich in der Mitte einer Lichtung über das Grab eines Zwergenkönigs. Ein steinernes Tor in der Nordseite versperrt Ihnen den Weg ins Innere. >steig vom pferd Ok >öffne das tor Sie öffnen das Tor. >gehe nach unten GRABKAMMER In der Mitte der engen Grabkammer steht ein Sarkophag. In der Ostwand befindet sich eine schwere, mit Eisen beschlagene Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine hinter der Grabkammer liegende Höhle. >öffne den rucksack Sie öffnen den Rucksack. >nimm die fackel aus dem rucksack Sie nehmen eine Fackel. >zünde die fackel an Ok >schließe das tor Ok >schau GRABKAMMER In der Mitte der engen Grabkammer steht ein Sarkophag. In der Ostwand befindet sich eine schwere, mit Eisen beschlagene Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine hinter der Grabkammer liegende Höhle. >lösche die fackel Ok >schau Finsternis Sie können hier kaum etwas sehen... >zünde die fackel an Ok >sw HÖHLE Als Sie die Höhle betreten springt Ihnen mit markerschütterndem Gebrüll ein Troll entgegen! >töte den troll mit dem schwert Mit einem schrecklichen Schrei stirbt der Troll, als Ihr Schwert ihn trifft. Als er zu Boden stürzt, fällt ein Schlüssel aus seinem Wams. >nimm den schlüssel Sie nehmen einen Schlüssel. >no GRABKAMMER In der Mitte der engen Grabkammer steht ein Sarkophag. In der Ostwand befindet sich eine schwere, mit Eisen beschlagene Eichentür. Im Südwesten führt ein schmaler Gang weiter in eine hinter der Grabkammer liegende Höhle. >öffne die tür (mit dem Schlüssel) Sie öffnen die Eichentür. >o SCHATZKAMMER In dieser Kammer lagerten die Grabbeigaben des Königs. >#ende Übrigens: Taschenlampen lassen sich leichter programmieren als Fackeln, weil sie ein- und ausgeschaltet werden und daher keine neuen Verben benötigen. Ich fand aber, daß Fackeln sich besser zu Demonstrationszwecken eignen. Das Mini-Adventure ist natürlich noch nicht perfekt. Fügen Sie ihm weitere Räume, NPCs und vor allem Aufgaben für den Spieler hinzu, die das Spiel interessanter machen. 8.5 Die Spielstandverwaltung ---------------------------- Mit den Metaverben #sichern und #laden kann der Spieler einen Spielstand speichern bzw. laden (s. 7.5). Wenn Sie in eine neue Objektdefinition auch neue Variablen aufnehmen, sollten Sie wissen, wie deren Werte dabei gesichert und wiederhergestellt werden. Die Verben #sichern und #laden führen zum Aufruf der Methoden TGame.UserSave bzw. TGame.UserLoad, in der vom Spieler der Name des betreffenden Spielstands angefordert wird. Die Namen und Anzahl der gespeicherten Spielstände werden in der Datei *.sav gespeichert, was das Auslesen aus dem Plattenverzeichnis erspart. Die einzelnen Spielstände werden in untypisierten Dateien mit der Blocklänge 1 und der Endung *.ttx gespeichert, wobei das x für die Nummer eines Spielstands in der Datei *.sav steht. Wenn ein Spielstand gespeichert werden soll, legt die Methode TGame.SaveGame() eine Datei *.ttx an und ruft die Methode Store(h : FILE); VIRTUAL; auf, die alle Nachfolger von TBasic besitzen. Mit dieser Methode speichert nun jedes Objekt die Variablen, deren Werte sich im Spielverlauf ändern können. Dabei werden alle Variablen als Byte-Wert codiert gespeichert. Ein untypisierter Dateityp wurde nur wegen eines inzwischen behobenen Bugs in FPC benutzt, eigentlich tut's auch ein File of Byte. Die Umwandlung der verschiedenen Variablentypen in Byte-Werte geschieht mit den Prozeduren WritePtr(VAR h : File; p : PBasic); WriteWord(VAR h : File; w : WORD); WriteBool(VAR h : File; v : Boolean); WriteDir(VAR h : File; d1 : TDir); Bytevariablen werden natürlich direkt mit BlockWrite() gespeichert. Der Pointer auf ein Objekt wird einfach als dessen ID gespeichert. Für andere Datentypen müssen Sie eigene Umwandlungen programmieren. Umgekehrt kann jedes Objekt mit der Methode Load(h : FILE); VIRTUAL; einen gespeicherten Zustand wiederherstellen. Hierfür werden die Funktionen und Prozeduren ReadPtr(VAR h : File) : POINTER; ReadWord(VAR h : File; VAR w : WORD); ReadBool(VAR h : File; VAR v : Boolean); ReadDir(VAR h : File; VAR d1 : TDir); verwendet. Wenn Sie in einer Objektdefinition nun eine neue Variable definieren, die sich im Spielverlauf ändern kann, so müssen Sie die Methoden Store() und Load() überschreiben, die neue Variable speichern bzw. laden und anschließend den Vorgänger des Objekttyps aufrufen, der sich um alle weiteren Variablen kümmert. Achten Sie darauf, daß die Reihenfolge der Variablen in Store() und Load() gleich bleibt, sonst führt das Kommando #laden unweigerlich zu einem Absturz. 8.6 Statuszeile, Farben und Funktionstasten ------------------------------------------- Wenn der Spieler die Statuszeile nicht mit dem Kommando #status ausgeschaltet hat, wird sie immer dann neu geschrieben, wenn sich der aktuelle Raum, die Punktzahl oder die Zahl der Spielzüge ändert. Letzteres passiert nach jeder Runde, in der der Spieler kein Metaverb eingegeben hat. Den Text in der Statuszeile liefern die Methoden TPlayer.StatusBarLeftStr : STRING; VIRTUAL und TPlayer.StatusBarRightStr : STRING; VIRTUAL Links wird normalerweise der Name des aktuellen Raums ausgegeben, während rechts die Punktzahl und die Zahl der bisherigen Spielrunden von einem Schrägstrich getrennt ausgegeben werden. Die Informationen hierüber sind in TPlayer gespeichert, weshalb diese Methoden nicht zu TGame gehören. Sie können diese Methoden überschreiben, um beliebige andere Informationen in der Statuszeile auszugeben. Die Bildschirmfarben können Sie mit der in tio.pas definierten Prozedur SetColor(fc,bc : BYTE) einstellen. Dabei gibt fc die Text- und bc die Hintergrundfarbe an. Als Textfarben sind alle 16 in tio.pas deklarierten Farbkonstanten verfügbar, während für die Hintergrundfarbe nur die Farben 0 (Black) bis 7 (Lightgray) verfügbar sind. Bei der Textausgabe können Sie mit den Sonderzeichen (s. 4.4) \h und \l die Farbintensität ändern, das Zeichen \r vertauscht die Text- und Hintergrundfarben. Leider führen nicht alle Farbkombinationen unter Windows, DOS und Linux zur selben Darstellung. Wenn Sie Ihr Spiel auf ein anderes System übertragen wollen, müssen Sie hier experimentieren. Mit den voreingestellten Farben gibt es keine Probleme. Um dem Spieler Tipparbeit abzunehmen, können Sie die Funktionstasten F1 bis F10 mit häufig benötigten Verben vorbelegen. Hierzu benutzen Sie die Prozedur SetFKey(n : BYTE; str : STRING) Der Parameter n gibt dabei die gewünschte Funktionstaste an. 8.7 Kommandozeilenoptionen -------------------------- Der Spieler kann das fertige Programm mit einigen Kommandozeilenoptionen aufrufen, um Voreinstellungen zu verändern. Jede Option beginnt dabei mit einem Minuszeichen gefolgt von einem Buchstaben und einer Zahl () oder einem Schalter (). Schalter werden mit dem Pluszeichen ein- und mit dem Minuszeichen ausgeschaltet. Die Optionen sind: >> F - bestimmt die Textfarbe >> B - bestimmt die Hintergrundfarbe >> R - bestimmt die Zahl der Bildschirmzeilen. Diese Zahl muß am Monitor bereits eingestellt sein, Textopia ändert keine Fenstergröße oder Bildschirmauflösung. Voreingestellt sind 25 Zeilen. >> C - bestimmt die Zahl der Zeichen pro Bildschirmzeile. Voreingestellt sind 80 Zeichen. >> S - bestimmt, ob das Spiel mit oder ohne Statuszeile gestartet wird. >> V - bestimmt, ob in bereits bekannten Räumen die ausführliche Raumbeschreibung oder nur der Raumname ausgegeben wird. >> U - bestimmt, ob die Eingabe in Großbuchstaben umgewandelt wird. Die Option -? gibt einen kurzen Hilfetext aus. Die Berücksichtigung der Kommandozeilenoptionen können Sie mit der Zeile useroption:=False; verhindern. 8.8 Umlaute ----------- DOS, Windows und Linux gebrauchen unterschiedliche Zeichensätze. Daher entsteht bei der Übertragung von Programmen zwischen den Systemen das Problem, daß die deutschen Umlaute nicht als solche ausgegeben werden. Um dieses Problem zu umgehen, sind in tstring.pas die Char-Konstanten lae_kc, loe_kc, lue_kc und ss_kc für die kleinen und uae_kc, uoe_kc, uue_kc für die großen Umlaute definiert. Die Werte der Konstanten sind dabei vom Compiler abhängig, so daß für eine Konstante auf jedem System das richtige Zeichen ausgegeben wird. Wenn Sie Ihr Programm nur für ein System schreiben, brauchen Sie sich um die Konstanten nicht zu kümmern und können in Ihren Texten die Umlaute direkt schreiben. Andernfalls haben Sie zwei Möglichkeiten, die Texte in Ihrem Spiel an unterschiedliche Zeichensätze anzupassen. Sie können die Umlaute wie in den Textopia-Quelltexten gegen ihre entsprechenden Konstanten austauschen. Dadurch werden die Texte allerdings recht unleserlich. In allen Texten, die mit der Prozedur Print() ausgegeben werden, können Sie aber auch statt eines Umlauts einen Backlash gefolgt von einem Vokal eingeben. Print() tauscht diese Zeichenfolge dann automatisch gegen die Konstante des entsprechenden Umlauts aus (aus ä wird also ä). Diese Ersetzung kann auch das Programm umlaut.pas für Sie vornehmen. Mit dem Aufruf umlaut -a dateiname werden alle Umlaute gegen einen Backlash und ihren Vokal getauscht. Mit umlaut -e dateiname wird aus dem Backlash wieder ein normaler Umlaut. 8.9 Warnungen ------------- Wenn Sie bei der Initialisierung der Objekte einen Fehler machen, gibt Textopia in spitzen Klammern eine Warnung aus und fragt Sie, ob Sie das Programm fortsetzen möchten. Folgende Warnungen sind möglich: Keine Verbindung in xxx Sie haben in TRoom.IsGate eine Richtung angegeben, in der keine Verbindung existiert. Objekt xxx kann kein Schloß zugewiesen werden Nur Objekten vom Typ PLink und PItem können Schlösser zugewiesen werden. xxx und yyy sind bereits verbunden Sie haben versucht, zwei Räume doppelt zu verbinden. xxx kann nicht ein-/ausgeschaltet werden Die Methoden TItem.SwitchOn und TItem.SwitchOff funktionieren nur, wenn das Objekt das Attribut switchable_at hat. Objekt xxx kann nicht in yyy abgelegt werden Mit TItem.MoveItemTo() können Objekte nur in Räume oder Behälter bewegt werden. Objekt xxx kann nicht bei Objekt yyy positioniert werden Im Konstruktor von TItem muß ein Raum oder ein Behälter als Position angegeben werden. Index xxx existiert nicht Es wurde auf einen ungültigen Index von TObjList zugegriffen. Ungültiger Spielerstatus Der Methode TPlayer.SetState() können nur die Konstanten dead_ps, alive_ps und victory_ps übergeben werden. Spieler kann nicht bei Objekt xxx positioniert werden TPlayer.MovePlayerTo() kann den Spieler nur in Räume oder Objekte mit dem Attribut enterable_at versetzen. Spielerobjekt nicht initialisiert Eine Instanz von TPlayer muß vor TGame initialisiert werden. Unbekanntes Token xxx Sie haben sich in einer mit TGame.AddVerb() angegebenen Grammatik verschrieben. xxx wurde nicht bearbeitet Ein Ereignis wurde nicht verarbeitet. Es erfolgte keine Rückmeldung durch return oder replay. ***************************************************************************** IX Die Units ***************************************************************************** 9.1 Funktionen und Prozeduren von tstring.pas --------------------------------------------- Es folgt eine Liste der im Interface-Abschnitt deklarierten Datentypen, Funktionen und Prozeduren. Einige andere Routinen in tstring.pas dienen obskuren internen Zwecken und werden hier nicht aufgeführt. *** Datentypen *** CONST {$IFNDEF fpc} { Umlaute unter Dos/Windows } lae_kc = #132; loe_kc = #148; lue_kc = #129; ss_kc = #225; uae_kc = #142; uoe_kc = #153; uue_kc = #154; {$ELSE} { Umlaute unter Linux } lae_kc = #228; loe_kc = #246; lue_kc = #252; ss_kc = #223; uae_kc = #196; uoe_kc = #214; uue_kc = #220; {$ENDIF} maxbuf = 10; { Max. Zwischenspeicher fuer Woerter } boolstr : ARRAY[BOOLEAN] OF STRING = ('Nein','Ja '); { Abkuerzungen fuer True/False } t = TRUE; f = FALSE; TYPE TAdress = (du,sie,ihr); { Anrede Spieler } TGender = (male,female); { Geschlecht Spieler/in } TCasus = (nom,acc,gen,dat); { Grammatische Faelle } TBuffer = ARRAY[1..maxbuf] OF STRING; *** Prozeduren und Funktionen *** PROCEDURE DelStr(VAR str : STRING; sub : STRING); Löscht alle Vorkommen von sub in str. FUNCTION LowCase(a : CHAR) : CHAR; Wandelt ein Zeichen in Kleinschrift. Berücksichtigt auch Umlaute. FUNCTION Lower(s1 : STRING) : STRING; Wandelt einen String in Kleinschrift. Berücksichtigt auch Umlaute. FUNCTION Noun(n1 : STRING; casus : TCasus; def : BOOLEAN; num : BYTE) : STRING; Dekliniert den Objeknamen n1 (s. 4.3). FUNCTION Numeral(n : BYTE) : STRING; Liefert ein Numeral für eine Zahl zwischen 2 und 10. FUNCTION NumToStr(n : WORD) : STRING; Wandelt n in einen String. FUNCTION PerPron(adress : TAdress; casus : TCasus) : STRING; Liefert das zu Anrede und Kasus passende Personalpronomen (Sie, Er etc.). FUNCTION RandStr(line : STRING) : STRING; Wählt zufällig einen der durch Semikolons getrennten Teilstrings von line aus und liefert ihn zurück. PROCEDURE SwpStr(VAR str : STRING; oldstr,newstr : STRING); Tauscht in str alle Vorkommen von oldstr gegen newstr. FUNCTION UpChar(a : CHAR) : CHAR; Wandelt ein Zeichen in Großschrift. Berücksichtigt auch Umlaute. FUNCTION Upper(s1 : STRING) : STRING; Wandelt einen String in Großschrift. Berücksichtigt auch Umlaute. 9.2 Funktionen und Prozeduren von tio.pas ----------------------------------------- Auch hier werden nur Datentypen, Prozeduren und Funktionen aufgeführt, die nicht ausschließlich für interne Zwecke gedacht sind. *** Datentypen *** TYPE PChain = ^TChain; { Element der String-Liste } TChain = RECORD c : Char; next : PChain; END; {$IFDEF tp} PDiskText = ^TDiskText; { Stringverwaltung mit Textdatei. Nur fuer TP/Dos } TDiskText = OBJECT CONSTRUCTOR Init; FUNCTION HasText : BOOLEAN; PROCEDURE SetText(str : STRING; clear : BOOLEAN); PROCEDURE PrintText; FUNCTION GetText : STRING; DESTRUCTOR Done; PRIVATE start : LONGINT; { Startposition des Textes in Datei } END; {$ENDIF} PMemText = ^TMemText; { Stringverwaltung mit Liste } TMemText = OBJECT CONSTRUCTOR Init; FUNCTION HasText : BOOLEAN; PROCEDURE SetText(str : STRING; clear : BOOLEAN); PROCEDURE PrintText; FUNCTION GetText : STRING; DESTRUCTOR Done; PRIVATE pfirst : PChain; { Startadresse des Textes im Speicher } PROCEDURE DelStr; END; PMStr = ^TMStr; TMStr = RECORD line : STRING[132]; next : PMStr; END; PMap = ^TMap; { Nimmt Karte fuers Automapping auf } TMap = OBJECT start : PMStr; CONSTRUCTOR Init; PROCEDURE InsStr(x1,y1 : SHORTINT; str : STRING); PROCEDURE Show; DESTRUCTOR Done; END; CONST back_kc = #08; { Tastaturcodes } del_kc = #83; down_kc = #80; end_kc = #79; enter_kc = #13; esc_kc = #27; f1_kc = #59; f2_kc = #60; f3_kc = #61; f4_kc = #62; f5_kc = #63; f6_kc = #64; f7_kc = #65; f8_kc = #66; f9_kc = #67; f10_kc = #68; home_kc = #71; ins_kc = #82; left_kc = #75; pdown_kc = #81; pup_kc = #73; right_kc = #77; up_kc = #72; { Farben, da im Spiel keine CRT gebraucht wird } black = 0; blue = 1; green = 2; cyan = 3; red = 4; magenta = 5; brown = 6; lightgray = 7; darkgray = 8; lightblue = 9; lightgree = 10; lightcyan = 11; lightred = 12; lightmagenta = 13; yellow = 14; white = 15; VAR maxcol, { Bildschirmspalten } maxrow, { Bildschirmzeilen } row : BYTE; { aktuelle Zeile } replay, { Bestaetigung fuer Textausgabe } debug : BOOLEAN; { Debug-Modus ein/aus } *** Prozeduren und Funktionen *** PROCEDURE Print(line : STRING); Gibt line ohne Zeilenvorschub aus. Um einen Zeilenvorschub zu erzeugen, muß am Ende von line ein \n eingefügt werden. Die Textausgabe in Ihrem Spiel sollte immer über Print() erfolgen. FUNCTION Question(str,chars : STRING) : CHAR; Gibt str aus und wartet auf einen Tastendruck, der ein in chars vorkommendes Zeichen liefern muß. Kann für eine Menüauswahl verwendet werden. PROCEDURE SetColor(fc,bc : BYTE); Setzt Text- und Hintergrundfarben, wobei fc die Text- und bc die Farbe des Hintergrunds angibt. PROCEDURE SetFKey(n : BYTE; str : STRING); Belegt die Funktionstaste n mit str. PROCEDURE SetWinTitle(title : STRING); Setzt unter Windows den Fenstertitel. Unter DOS und Linux wird diese Anweisung ignoriert. 9.3 Datentypen, Funktionen und Prozeduren von tmain.pas ------------------------------------------------------- Neben den Objekten TBasic, TItem, TRoom, TLink, TLock, TPlayer und TGame definiert tmain.pas auch die von den Objekten verwendeten Datentypen, die Funktion Adr() sowie die Prozeduren und Funktionen für die Spielstandverwaltung. *** Datentypen *** TYPE PBasic = ^TBasic; { Zeiger auf Basisobjekt } PLink = ^TLink; { Zeiger auf Verbindung } PGame = ^TGame; { Zeiger auf Spielobjekt } PLib = ^TVerb; { Zeiger auf Eintrag im Wörterbuch } PItem = ^TItem; { Zeiger auf Gegenstand } PLock = ^TLock; { Zeiger auf Schloss fuer Tueren und Behaelter } PNoun = ^TNoun; { Zeiger auf Informationen ueber Eingabe } PPlayer = ^TPlayer; { Zeiger auf Spielerobjekt } PRoom = ^TRoom; { Zeiger auf Raum } TDir = (north,northeast, { Himmelsrichtungen } east,southeast, south,southwest, west,northwest, up,down, nowhere); T_Class = (room,link,item); { Bausteine der Spielwelt } TMatter = (inanimate,dead,alive); { moegliche Zustaende der Items } { TNoun enthaelt Informationen ueber die in der Spielereingabe ausgewaehlten Objekte } TNoun = RECORD detect, adj : BOOLEAN; number : INTEGER; xroom : PRoom; xlink : PLink; xitem : PItem; END; { Record fuer Nachricht an Objekte } TEvent = RECORD action, { Event-Konstante des Ereignisses } exec, { Wie oft ausgefuehrt? } maxexec : BYTE; { Wie oft maximal ausfuehren? } return : BOOLEAN; { erfolgreich ausgefuehrt? } who : PItem; { Akteur: Spieler oder NPC } first, { Empfaengerobjekt } second : PNoun; { Weiteres beteiligtes Objekt } data : POINTER; { Zeiger auf zusaetzliche Daten } END; { TWeight verwaltet Gewichte } TWeight = OBJECT CONSTRUCTOR Init(_owner : POINTER; _min,_max : BYTE); FUNCTION GetCont : BYTE; { Zahl enthaltener Objekte } FUNCTION GetSum : BYTE; { Gewicht } FUNCTION GetMax : BYTE; { Maximalgewicht } DESTRUCTOR Done; PRIVATE owner : POINTER; { Item oder Spieler } counter, { Anzahl enthaltener Items } wsum, { Eigengewicht + Gewicht von Items } wmax : BYTE; { Maximalgewicht } PROCEDURE Pick(x : POINTER; VAR error : BOOLEAN); PROCEDURE Drop(x : POINTER); END; { Spielerobjekt } TPlayer = OBJECT CONSTRUCTOR Init(_where : WORD; _adress : TAdress; _gender : TGender; _wmin,_wmax : BYTE); PROCEDURE AfterLife; VIRTUAL; FUNCTION GetAdress : TAdress; FUNCTION GetContainer : PItem; FUNCTION GetContent : BYTE; FUNCTION GetGender : TGender; FUNCTION GetLocation : PRoom; FUNCTION GetMaxWeight : BYTE; FUNCTION GetScores : WORD; FUNCTION GetMoves : WORD; FUNCTION GetState : BYTE; FUNCTION GetWeight : BYTE; FUNCTION HasPlayer(i : WORD) : BOOLEAN; PROCEDURE IncScores(ds : WORD); PROCEDURE Load(VAR h : File); VIRTUAL; PROCEDURE MovePlayerTo(where : PBasic); PROCEDURE SetState(s : BYTE); FUNCTION StatusBarLeftStr : STRING; VIRTUAL; FUNCTION StatusBarRightStr : STRING; VIRTUAL; PROCEDURE Store(VAR h : File); VIRTUAL; FUNCTION RankStr : STRING; VIRTUAL; PROCEDURE Victory; VIRTUAL; DESTRUCTOR Done; PRIVATE inside : PItem; { Zeiger auf Behaelter oder Fahrzeug } position : PRoom; { Zeiger auf aktuellen Raum } weight : TWeight; adress : TAdress; { Anrede fuer Spieler } gender : TGender; { Spielergeschlecht } moves, { Runden } scores : WORD; { Punkte } state : BYTE; { Status des Spielers } PROCEDURE Enter(_item : PItem); PROCEDURE IncMoves; PROCEDURE Inventory; PROCEDURE Leave; END; { Basisobjekt fuer Raeume, Durchgaenge und Items } TBasic = OBJECT name : TMemText; { Objektname als Listenobjekt } lock : PLock; { Schlosser fuer Links und Items } CONSTRUCTOR Init(_id : WORD; _name : STRING; _objclass : T_Class); PROCEDURE AddText(str : STRING); PROCEDURE DelText; FUNCTION GetClass : T_Class; FUNCTION GetCopies : BYTE; FUNCTION GetID : WORD; PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION HasDaemon : BOOLEAN; FUNCTION HasTimer : BOOLEAN; PROCEDURE Inspect; VIRTUAL; PROCEDURE Load(VAR h : File); VIRTUAL; PROCEDURE MyText; VIRTUAL; PROCEDURE RunDaemon; VIRTUAL; FUNCTION Scope : BYTE; VIRTUAL; PROCEDURE StartDaemon; PROCEDURE StopDaemon; PROCEDURE StartTimer(rounds : BYTE); PROCEDURE StopTimer; PROCEDURE Store(VAR h : File); VIRTUAL; PROCEDURE TimeOut; VIRTUAL; PROCEDURE View; VIRTUAL; DESTRUCTOR Done; VIRTUAL; PRIVATE _class : T_Class; { room, item oder link } id : WORD; { ID zur eindeutigen Identifikation } copies, { Anzahl erreichbarer Objekte mit gleichem Namen } counter : BYTE; { Zaehler fuer Timer } flag, { wichtig } daemon, { Daemon eingeschaltet? } timer, { Timer eingeschaltet? } tracing : BOOLEAN; { Objektzustand verfolgen? } {$IFDEF tp} objtext : PDiskText; { TP: Text in Datei speichern } {$ELSE} objtext : PMemText; { Delphi/FPC: Text in Liste speichern } {$ENDIF} PROCEDURE ObscureEvents; PROCEDURE RunTimer; PROCEDURE Reset; PROCEDURE TextOut; END; { TLock: Schloesser fuer Tueren und Behaelter } TState = (open,closed,locked); { Zustaende fuer Schloesser } TLock = OBJECT CONSTRUCTOR Init(_owner : PBasic; _openable, _closeable : BOOLEAN; _state : TState; _key : PItem); FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION GetKey : PItem; FUNCTION GetOwner : PBasic; FUNCTION GetState : TState; PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION HasKey(event : Tevent) : BOOLEAN; FUNCTION IsOpenable : BOOLEAN; FUNCTION IsCloseable : BOOLEAN; PROCEDURE Load(VAR h : File); VIRTUAL; PROCEDURE SetCloseable(s : BOOLEAN); PROCEDURE SetOpenable(s : BOOLEAN); PROCEDURE SetState(s : TState); PROCEDURE Store(VAR h : File); VIRTUAL; PROCEDURE View; VIRTUAL; DESTRUCTOR Done; VIRTUAL; PRIVATE key : PItem; { Instanz von TItem oder NIL } owner : PBasic; { Instanz von TItem oder TLink } openable, { kann das Schloss geoeffnet und } closeable : BOOLEAN; { geschlossen werden? } state : TState; { offen, geschlossen oder verschlossen } END; { TLink stellt eine Verbindung zwischen jeweils zwei Raeumen her } TLink = OBJECT(TBasic) r1,r2 : PRoom; CONSTRUCTOR Init(_id : WORD; _name : STRING; _from : WORD; _dirto : TDir; _to : WORD; _show : BOOLEAN); FUNCTION BeforeAction(VAR event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION AfterAction(VAR event : TEvent) : BOOLEAN; VIRTUAL; PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION HasAutoDescription : BOOLEAN; PROCEDURE Load(VAR h : File); VIRTUAL; FUNCTION Scope : BYTE; VIRTUAL; PROCEDURE Store(VAR h : File); VIRTUAL; PROCEDURE View; VIRTUAL; DESTRUCTOR Done; VIRTUAL; PRIVATE show : BOOLEAN; { Objekt in Raumbeschreibung erwaehnen? } END; { TRoom, der Objekttyp fuer alle Raeume } TRoom = OBJECT(TBasic) gate : ARRAY[north..down] OF PLink; CONSTRUCTOR Init(_id : WORD; _name : STRING); FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION FromDir : TDir; PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION HasLight : BOOLEAN; FUNCTION IsGate(d : TDir) : TState; PROCEDURE Load(VAR h : File); VIRTUAL; PROCEDURE MyDarkness; VIRTUAL; PROCEDURE SetLight(_light : BOOLEAN); PROCEDURE Store(VAR h : File); VIRTUAL; FUNCTION Scope : BYTE; VIRTUAL; FUNCTION ToDir : TDir; PROCEDURE View; VIRTUAL; DESTRUCTOR Done; VIRTUAL; PRIVATE light, { Kann Spieler hier etwas sehen? } explored : BOOLEAN; { Deja-vu? } wto,wfrom : TDir; { Spieler will wohin / kommt woher? } PROCEDURE CountLinks; PROCEDURE RoomDescription; END; { TAttrib dient der Verwaltung von Item-Attributen in einer Liste } PAttrib = ^TAttrib; TAttrib = RECORD attribute : BYTE; prev,next : PAttrib; END; { TItem stellt alle Gegenstaende im Spiel dar } TItem = OBJECT(TBasic) CONSTRUCTOR Init(_id : WORD; _name : STRING; _location : PBasic; _show : BOOLEAN; _wmin,_wmax : BYTE); FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; FUNCTION Contains(x : PItem) : BOOLEAN; FUNCTION DecAmount : BOOLEAN; FUNCTION GetAmount : BYTE; FUNCTION GetContainer : PItem; FUNCTION GetContent : BYTE; FUNCTION GetLocation : PRoom; FUNCTION GetMatter : TMatter; FUNCTION GetMaxWeight : BYTE; FUNCTION GetWeight : BYTE; PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION Has(p : BYTE) : BOOLEAN; FUNCTION HasAutoDescription : BOOLEAN; FUNCTION IsOn : BOOLEAN; FUNCTION IsOff : BOOLEAN; PROCEDURE ListMe(s : BOOLEAN); PROCEDURE Load(VAR h : File); VIRTUAL; PROCEDURE MoveCmd(where : POINTER; VAR event : TEvent); PROCEDURE MoveItemTo(where : PBasic); FUNCTION Scope : BYTE; VIRTUAL; PROCEDURE SetAttrib(a : BYTE; _on : BOOLEAN); PROCEDURE SetMatter(state : TMatter); PROCEDURE SetPraepos(pp : STRING); PROCEDURE SetAmount(n : BYTE); PROCEDURE Store(VAR h : File); VIRTUAL; PROCEDURE SwitchOn; PROCEDURE SwitchOff; PROCEDURE View; VIRTUAL; DESTRUCTOR Done; VIRTUAL; PRIVATE location : PRoom; inside : PItem; weight : TWeight; matter : TMatter; { Objektzustand? } pstart : PAttrib; { Startadresse Eigenschaften } show, { In Raumbeschreibung erwaehnen? } on : BOOLEAN; { Schalterzustand } amount : BYTE; { Menge,Tankfuellung etc. } praepos : STRING[4]; { Praeposition } PROCEDURE ClearAttributes; PROCEDURE Follow; PROCEDURE Register; END; { TVerb bildet das Woerterbuch } TVerb = RECORD verb,syntax : TMemText; { Verb und zugehoerige Tokenfolge } message : BYTE; { Vom Verb ausgeloestes Ereignis } prev,next : PLib; { vorheriges/naechstes Verb } END; { Hauptobjekt: Steuert Dialog mit Spieler, interpretiert dessen Eingabe und versendet Nachrichten an betroffene Objekte } TGame = OBJECT CONSTRUCTOR Init(_statusline,_upsize,_verbose : BOOLEAN; _history : BYTE); FUNCTION ActorWhere : PRoom; FUNCTION ActorInside(container : PItem) : BOOLEAN; PROCEDURE AddProlog(_str : STRING); PROCEDURE AddVerb(_verb,_syntax : STRING; _event : BYTE); PROCEDURE Admonition(adress : TAdress); VIRTUAL; FUNCTION AfterAction(event : TEvent) : BOOLEAN; FUNCTION BeforeAction(event : TEvent) : BOOLEAN; PROCEDURE BeforeParsing(VAR input : STRING); VIRTUAL; PROCEDURE ClearDictionary; PROCEDURE DelVerb(_event : BYTE); FUNCTION GetActor : PItem; PROCEDURE GetTime(VAR hour,min : BYTE); PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; FUNCTION MetaVerb(mv : BYTE) : BOOLEAN; VIRTUAL; FUNCTION MyScope(_id : WORD; _action : BYTE) : BOOLEAN; VIRTUAL; PROCEDURE ParserError(n : BYTE; str : STRING; i : BYTE; VAR error : BOOLEAN); VIRTUAL; PROCEDURE Run; PROCEDURE SetTime(_time,_rate : WORD); PROCEDURE WriteProlog; DESTRUCTOR Done; PRIVATE {$IFDEF tp} prologue : PDiskText; { Vorspann } {$ELSE} prologue : PMemText; {$ENDIF} statusline, { Statuszeile anzeigen? } verbose, { Ausfuehrliche Beschreibung? } upsize, { Eingabe in Grossschrift? } meta : BOOLEAN; { Ist erkanntes Verb ein Metaverb? } history, { Anzahl Zeilen im Historypuffer } quit, { Spielende erreicht? } maxwords : BYTE; { Zahl eingegebener Woerter } time, { Zeit in Minuten seit 0:00 Uhr } rate : WORD; { Minuten pro Runde } buffer : TBuffer; { Eingabepuffer } actor : PItem; { Akteuer } pverb : PLib; { Startadresse Woerterbuch } refpro : PNoun; { Substantiv fuer Reflexivpronomen } nounstr : ARRAY[1..maxbuf] OF TNoun; { wichtig } PROCEDURE Browser; PROCEDURE CheckCommandLine; FUNCTION CmpScope(index,ttype,_action : BYTE) : BOOLEAN; PROCEDURE DetectNumeral(w0 : BYTE; VAR number : INTEGER; VAR ok : BOOLEAN); PROCEDURE DrawMap; PROCEDURE DrawStatusline(location : BOOLEAN); PROCEDURE InitEvent(VAR event : TEvent; _what, _maxexec : BYTE; _who : PItem; _first, _second : PNoun; _data : POINTER); FUNCTION IsArtOrPron(str : STRING) : BOOLEAN; PROCEDURE LoadGame(n : BYTE); PROCEDURE Mapping(p : PRoom; x0,y0 : SHORTINT; VAR map : PMap); PROCEDURE Parse(line : STRING); PROCEDURE QuitGame; PROCEDURE ReadLib; PROCEDURE SaveGame(n : BYTE); PROCEDURE SearchNoun(VAR w0 : BYTE; t0 : BYTE; VAR tfound : BYTE; ttype,_action : BYTE; VAR userstr : STRING; VAR pfailed : PBasic; VAR error : BOOLEAN); FUNCTION IsVerbose : BOOLEAN; PROCEDURE UserLoad; PROCEDURE UserSave; END; CONST sdstr : ARRAY[0..4] OF STRING[12] { Namen derScope-Definitionen } = ('notinroom_sd', 'visible_sd', 'reachable_sd', 'bynpc_sd', 'held_sd'); { PlayerState: Spiel wird beendet, wenn Status des Spielers <> alive_ps } dead_ps = 0; alive_ps = 1; victory_ps = 2; { Scope-Definitionen } notinroom_sd = 0; { Objekt ist ausser Sicht und Reichweite } visible_sd = 1; { Objekt ist sichtbar } reachable_sd = 2; { Objekt ist sichtbar und erreichbar } bynpc_sd = 3; { Objekt wird von NPC gehalten } held_sd = 4; { Objekt wird vom Spieler gehalten } { Vordefinierte Ereignisse } close_ev = 100; dance_ev = 101; dec_ev = 102; drink_ev = 103; eat_ev = 104; enter_ev = 105; examine_ev = 106; give_ev = 107; go_ev = 108; inv_ev = 109; drop_ev = 110; jump_ev = 111; kill_ev = 112; kiss_ev = 113; leave_ev = 114; lista_ev = 115; lists_ev = 116; load_ev = 117; lock_ev = 118; look_ev = 119; map_ev = 120; open_ev = 121; press_ev = 122; quit_ev = 123; restart_ev = 124; read_ev = 125; score_ev = 126; save_ev = 127; set1_ev = 128; set2_ev = 129; show_ev = 130; sleep_ev = 131; smell_ev = 132; switchoff_ev = 133; switchon_ev = 134; take_ev = 135; tell_ev = 136; touch_ev = 137; trace_ev = 138; wait_ev = 139; { vordefinierte Item-Eigenschaften } drinkable_at = 0; edable_at = 1; enterable_at = 2; moveable_at = 3; readable_at = 4; shining_at = 5; switchable_at = 6; takeable_at = 7; talkable_at = 8; transparent_at = 9; VAR top : PRoom; { Zeiger auf Raum 0 } useroption : BOOLEAN; { Kommandozeilenoptionen ein/aus } *** Prozeduren und Funktionen *** FUNCTION Adr(_id : WORD) : POINTER; Liefert einen Zeiger auf das Objekt mit der ID _id. Kann mit einer expliziten Typumwandlung in einen Zeiger vom Typ PBasic, PRoom, PItem oder PLink umgewandelt werden: PRoom(Adr(forest_id)). PROCEDURE Prepare(_filename : STRING; _new : BOOLEAN); Bestimmt Dateinamen für das Spiel und initialisiert den als Vesteck für Objekte dienenden Raum 0 (top). PROCEDURE WritePtr(VAR h : File; p : PBasic); Speichert einen Zeiger auf einen Nachfolger von TBasic in der Spielstanddatei h. FUNCTION ReadPtr(VAR h : File) : POINTER; Liest einen Zeiger auf einen Nachfolger von TBasic aus der Spielstanddatei h. PROCEDURE WriteWord(VAR h : File; w : WORD); Speichert einen Word-Wert in der Spielstanddatei h. PROCEDURE ReadWord(VAR h : File; VAR w : WORD); Liest einen Word-Wert aus der Spielstanddatei h. PROCEDURE WriteBool(VAR h : File; v : Boolean); Speichert einen Boolean-Wert in der Spielstanddatei h. PROCEDURE ReadBool(VAR h : File; VAR v : Boolean); Liest einen Boolean-Wert aus der Spielstanddatei h. PROCEDURE WriteDir(VAR h : File; d1 : TDir); Speichert einen TDir-Wert in der Spielstanddatei h. PROCEDURE ReadDir(VAR h : File; VAR d1 : TDir); Liest einen TDir-Wert aus der Spielstanddatei h. 9.4 Methoden von TBasic ----------------------- TBasic stellt das Basisobjekte für alle Objekte der Spielwelt dar. Von ihm werden die Objekttypen TItem, TRoom und TLink abgeleitet. Die privaten Methoden von TBasic und seiner Nachfolger werden hier nicht aufgeführt, sind aber im Quelltext kommentiert. CONSTRUCTOR Init(_id : WORD; _name : STRING; _objclass : T_Class); Initialisiert ein Objekt. Wird bei der Initialisierung aller Nachfolger von TBasic aufgerufen. PROCEDURE AddText(str : STRING); Ordnet dem Objekt eine Beschreibung zu. Mehrere Methodenaufrufe können einen Text mit mehr als 255 Zeichen zuordnen. PROCEDURE DelText; Entfernt die mit AddText() hinzugefügte Beschreibung wieder. FUNCTION GetClass : T_Class; Liefert den Typ eines Objekts: item, room oder link. FUNCTION GetCopies : BYTE; Liefert die Anzahl der Objekte, die sich im gleichen Raum befinden und den gleichen Namen haben. FUNCTION GetID : WORD; Liefert die ID des Objekts. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Ereignisroutine (s. 6.1). Verarbeitet die Ereignisse dance_ev, dec_ev, examine_ev, jump_ev, lists_ev, touch_ev und trace_ev. Diese Ereignisse werden von den Ereignisroutinen der Nachfolger von TBasic weitergereicht und können auch dort behandelt werden. FUNCTION HasDaemon : BOOLEAN; Ist True, wenn der Dämon des Objekts aktiv ist. FUNCTION HasTimer : BOOLEAN; Ist True, wenn der Zeitschalter des Objekts aktiv ist. PROCEDURE Inspect; VIRTUAL; Gibt nach jeder Runde Informationen aus, wenn das Objekt mit .trace (s. 7.6) verfolgt wird. PROCEDURE Load(VAR h : File); VIRTUAL; Lädt gespeicherten Zustand. PROCEDURE MyText; VIRTUAL; Gibt variable Beschreibung aus, wenn dem Objekt mit AddText() kein Text zugeordnet wurde. PROCEDURE RunDaemon; VIRTUAL; Führt einen aktiven Hintergrundprozeß (s. 8.3) aus. FUNCTION Scope : BYTE; VIRTUAL; Liefert Information über die Erreichbarkeit des Objekts (s. 5.7). PROCEDURE StartDaemon; Aktiviert den Hintergrundprozeß des Objekts. PROCEDURE StopDaemon; Beendet den Hintergrundprozeß des Objekts. PROCEDURE StartTimer(time : BYTE); Aktiviert den Zeitschalter des Objekts (s. 8.3). PROCEDURE StopTimer; Beendet den Zeitschalter vorzeitig. PROCEDURE Store(VAR h : File); VIRTUAL; Speichert den aktuellen Zustand. PROCEDURE TimeOut; VIRTUAL; Wird von abgelaufenem Zeitschalter ausgeführt. PROCEDURE View; VIRTUAL; Gibt Informationen für den von .browse und .list aktivierten Objekt-Betrachter aus. Wird von den Nachfolgern von TBasic überschrieben. DESTRUCTOR Done; VIRTUAL; Löscht dynamische Variablen. 9.5 Methoden von TItem ---------------------- TItem ist ein Nachfolger von TBasic und kann fast alle möglichen und unmöglichen Dinge in der Spielwelt darstellen. CONSTRUCTOR Init(_id : WORD; _name : STRING; _location : PBasic; _show : BOOLEAN; _wmin,_wmax : BYTE); Initialisiert einen Gegenstand (s. 5.1). FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TItem.HandleEvent() nach Ausführung eines Ereignisses aufgerufen und verhindert eine Textausgabe, wenn False zurückgegeben wird. FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TItem.HandleEvent() vor Ausführung eines Ereignisses aufgerufen und verhindert eine weitere Bearbeitung, wenn False zurückgegeben wird. FUNCTION Contains(x : PItem) : BOOLEAN; Stellt fest, ob das Objekt enthalten ist. FUNCTION DecAmount : BOOLEAN; Reduziert die Menge des Objekts um eine Einheit. Wird bislang nur von den Ereignissen eat_ev und drink_ev verwendet (s. SetAmount()). FUNCTION GetAmount : BYTE; Liefert die Menge des Objekts. FUNCTION GetContainer : PItem; Liefert den Behälter des Objekts. FUNCTION GetContent : BYTE; Liefert Anzahl enthaltener Objekte. FUNCTION GetLocation : PRoom; Liefert den Raum, in dem sich das Objekt (und evtl. sein Behälter) befindet. FUNCTION GetMatter : TMatter; Liefert den Zustand des Objekts: inanimate, dead oder alive. FUNCTION GetMaxWeight : BYTE; Liefert das Maximalgewicht. FUNCTION GetWeight : BYTE; Liefert das Gewicht. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Ereignisroutine für die Ereignisse cose_ev, drink_ev, drop_ev, eat_ev, enter_ev, give_ev, kill_ev, kiss_ev, leave_ev, lock_ev, open_ev, read_ev, show_ev, switchoff_ev, switchon_ev, press_ev, take_ev und tell_ev. FUNCTION Has(p : BYTE) : BOOLEAN; Stellt fest, ob das Objekt das Attribut p hat. p ist der Wert einer Attributskonstante mit der Endung *_at. FUNCTION HasAutoDescription : BOOLEAN; Stellt fest, ob das Objekt nach der Raumbeschreibungen automatisch erwähnt wird. FUNCTION IsOn : BOOLEAN; Stellt fest, ob das Objekt eingeschaltet ist, wenn es das Attribut switchable_at hat. FUNCTION IsOff : BOOLEAN; Stellt fest, ob das Objekt ausgeschaltet ist, wenn es das Attribut switchable_at hat. PROCEDURE ListMe(s : BOOLEAN); Wenn True übergeben wird, wird das Objekt nach der Raumbeschreibung automatisch erwähnt. Ansonsten kann es in der Raumbeschreibung erwähnt oder vor dem Spieler versteckt werden. PROCEDURE Load(VAR h : File); VIRTUAL; s. TBasic.Load(). PROCEDURE MoveCmd(where : POINTER; VAR event : TEvent); Wird von TItem.HandleEvent() für die Bearbeitung der Ereignisse give_ev, drop_ev und take_ev aufgerufen und bewegt das Objekt an die durch where angegebene Position. Dabei werden BeforeAction() und AfterAction() aufgerufen. Wenn Sie die Position eines Gegenstandes ändern wollen, sollten Sie die Methode MoveItemTo verwenden(). PROCEDURE MoveItemTo(where : PBasic); Bewegt das Objekt an die durch where angegebene Position, dabei muß where ein Zeiger auf einen Raum oder einen Behälter sein. BeforeAction() und AfterAction() werden nicht aufgerufen. Die Positionsveränderung kann vom Spieler unbemerkt erfolgen. FUNCTION Scope : BYTE; VIRTUAL; s. TBasic.Scope. PROCEDURE SetAttrib(a : BYTE; _on : BOOLEAN); Setzt oder entfernt das Attribut a (s. 5.1). PROCEDURE SetMatter(state : TMatter); Setzt den Zustand des Objekts auf inanimate, dead oder alive. PROCEDURE SetPraepos(pp : STRING); Bestimmt die Präposition (meist "auf" oder "in"), wenn der Spieler das Objekt betreten kann (s. 5.1). PROCEDURE SetAmount(n : BYTE); Bestimmt eine Menge, die bislang nur von den Ereignissen eat_ev und drink_ev verringert wird. Vulgo: Diese Menge gibt an, wie oft der Spieler von einem Brot essen oder aus einer Flasche trinken kann. Die Voreinstellung ist eins. PROCEDURE Store(VAR h : File); VIRTUAL; Speichert aktuellen Objektzustand. PROCEDURE SwitchOn; Schaltet das Objekt ein. Das Attribut switchable_ev muß vorhanden sein. PROCEDURE SwitchOff; Schaltet das Objekt aus. Das Attribut switchable_ev muß vorhanden sein. PROCEDURE View; VIRTUAL; Gibt Informationen für den mit .browse und .list aktivierten Objekt-Betrachter aus. PROCEDURE Done; VIRTUAL; s. TBasic.Done. 9.6 Methoden von TRoom ---------------------- TRoom ist ein Nachfolger von TBasic und stellt einzelne Räume dar. CONSTRUCTOR Init(_id : WORD; _name : STRING); Initialisiert einen Raum (s. 4.3). FUNCTION FromDir : TDir; Stellt fest, aus welcher Richtung der Spieler den Raum betreten hat. Wenn die Methode den Wert nowhere liefert, war der Spieler hier noch nie oder hat das Spiel begonnen. FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TRoom.HandleEvent() nach Ausführung eines Ereignisses aufgerufen und verhindert eine Textausgabe, wenn False zurückgegeben wird. FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TRoom.HandleEvent() vor Ausführung eines Ereignisses aufgerufen und verhindert eine weitere Bearbeitung, wenn False zurückgegeben wird. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Ereignisroutine für die Ereignisse go_ev und look_ev. FUNCTION HasLight : BOOLEAN; Stellt fest, ob der Spieler hier sehen kann (s. 8.4). FUNCTION IsGate(d : TDir) : TState; Stellt fest, ob eine Verbindung/Tür in der angegebenen Richtung geöffnet (open), geschlossen (closed) oder verschlossen (locked) ist. PROCEDURE Load(VAR h : File); VIRTUAL; s. TBasic.Load(). PROCEDURE MyDarkness; VIRTUAL; Gibt einen Text an Stelle der Raumbeschreibung aus, wenn der Raum nicht beleuchtet ist. PROCEDURE SetLight(_light : BOOLEAN); Schaltet die Beleuchtung ein und aus (s. 8.4). PROCEDURE Store(VAR h : File); VIRTUAL; s. TBasic.Store(). FUNCTION Scope : BYTE; VIRTUAL; Der Scope eines Raums ist visible_sd, wenn der Spieler sich in ihm aufhält, oder notinroom_sd, wenn er sich in einem anderen Raum aufhält. FUNCTION ToDir : TDir; Stellt fest, in welche Richtung der Spieler den Raum verlassen will oder verlassen hat. Wenn diese Methode nowhere zurückliefert war der Spieler hier noch nie oder hat das Spiel hier begonnen und den Raum noch nicht verlassen. PROCEDURE View; VIRTUAL; s. TBasic.View. DESTRUCTOR Done; VIRTUAL; s. TBasic.Done. 9.7 Methoden von TLink ---------------------- TLink ist ein Nachfolger von TBasic und verbindet zwei Räume. CONSTRUCTOR Init(_id : WORD; _name : STRING; _from : WORD; _dirto : TDir; _to : WORD; _show : BOOLEAN); Initialisiert eine Verbindung (s. 5.6). FUNCTION BeforeAction(VAR event : TEvent) : BOOLEAN; VIRTUAL; Wird von TLink.HandleEvent() vor Ausführung eines Ereignisses aufgerufen und verhindert eine weitere Bearbeitung, wenn False zurückgegeben wird. FUNCTION AfterAction(VAR event : TEvent) : BOOLEAN; VIRTUAL; Wird von TLink.HandleEvent() nach Ausführung eines Ereignisses aufgerufen und verhindert eine Textausgabe, wenn False zurückgegeben wird. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Gibt die Ereignisse close_ev, lock_ev und open_ev an das zugeordnete Objekt vom Typ TLock weiter. FUNCTION HasAutoDescription : BOOLEAN; Stellt fest, ob die Verbindung (als Ausgang) nach der Raumbeschreibung erwähnt wird. PROCEDURE Load(VAR h : File); VIRTUAL; s. TBasic.Load(). FUNCTION Scope : BYTE; VIRTUAL; s. TBasic.Scope(). PROCEDURE Store(VAR h : File); VIRTUAL; s. TBasic.Store(). PROCEDURE View; VIRTUAL; s. TBasic.View. DESTRUCTOR Done; VIRTUAL; s. TBasic.Done(). 9.8 Methoden von TLock ---------------------- TLock dient Behältern und Verbindungen als Schloß. CONSTRUCTOR Init(_owner : PBasic; _openable,_closeable : BOOLEAN; _state : TState; _key : PItem); Initialisiert ein Schloß und ordnet es einem Gegenstand oder einer Verbindung zu (s. 5.2). FUNCTION AfterAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TLock.HandleEvent() nach Ausführung eines Ereignisses aufgerufen und verhindert eine Textausgabe, wenn False zurückgegeben wird. FUNCTION BeforeAction(event : TEvent) : BOOLEAN; VIRTUAL; Wird von TLock.HandleEvent() vor Ausführung eines Ereignisses aufgerufen und verhindert eine weitere Bearbeitung, wenn False zurückgegeben wird. FUNCTION GetKey : PItem; Liefert einen Zeiger auf das Objekt, mit dem sich das Schloß auf- und abschließen läßt. FUNCTION GetOwner : PBasic; Liefert einen Zeiger auf ein Objekt vom Typ PItem oder PLink, dem das Schloß zugeordnet ist. FUNCTION GetState : TState; Liefert den Zustand des Schlosses: open, closed oder locked. Alternativ kann auch die Methode TRoom.IsGate() verwendet werden. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Ereignisroutine für die Ereignisse close_ev, lock_ev und open_ev. FUNCTION HasKey(event : Tevent) : BOOLEAN; Stellt fest, ob der Akteur den für das Schloß benötigten Schlüssel trägt. FUNCTION IsOpenable : BOOLEAN; Stellt fest, ob sich das Schloß öffnen läßt. FUNCTION IsCloseable : BOOLEAN; Stellt fest, ob sich das Schloß schließen und abschließen läßt. PROCEDURE Load(VAR h : File); VIRTUAL; Speichert aktuellen Objektzustand. PROCEDURE SetCloseable(s : BOOLEAN); Bestimmt, ob sich das Objekt schließen und/oder abschließen läßt. PROCEDURE SetOpenable(s : BOOLEAN); Bestimmt, ob sich das Objekt öffnen läßt. PROCEDURE SetState(s : TState); Bestimmt den Zustand des Schlosses: open, closed oder locked. PROCEDURE Store(VAR h : File); VIRTUAL; Lädt gespeicherten Zustand. PROCEDURE View; VIRTUAL; Gibt Informationen im Objekt-Betrachter aus. DESTRUCTOR Done; VIRTUAL; Löscht dynamische Variablen. 9.9 Methoden von TPlayer ------------------------ TPlayer verwaltet die den Spieler betreffenden Informationen. CONSTRUCTOR Init(_where : WORD; _adress : TAdress; _gender : TGender; _wmin,_wmax : BYTE); Initialisiert das Spielerobjekt (s. 4.5). PROCEDURE AfterLife; VIRTUAL; Wird nach dem Tod des Spielers aufgerufen, um eine beliebige Meldung auszugeben. FUNCTION GetAdress : TAdress; Liefert die im Konstruktor angegebene Anrede für den Spieler: du, sie ihr. FUNCTION GetContainer : PItem; Liefert einen Zeiger auf das Objekt, in dem der Spieler sich befindet. FUNCTION GetContent : BYTE; Liefert die Anzahl der Objekte im Inventar des Spielers. FUNCTION GetGender : TGender; Liefert das im Konstruktor angegeben Geschlecht des Spielers: male oder female. FUNCTION GetLocation : PRoom; Liefert den aktuellen Raum. FUNCTION GetMaxWeight : BYTE; Liefert das Maximalgewicht, das der Spieler tragen kann. FUNCTION GetScores : WORD; Liefert die Punktzahl des Spielers. FUNCTION GetMoves : WORD; Liefert die Zahl der bisherigen Spielzüge. FUNCTION GetState : BYTE; Liefert den Zustand des Spielers: alive_ps, dead_ps oder victory_ps. FUNCTION GetWeight : BYTE; Liefert das Gewicht des Spielers. FUNCTION HasPlayer(i : WORD) : BOOLEAN; Stellt fest, ob sich das Objekt mit der ID i im Inventar befindet. PROCEDURE IncScores(ds : WORD); Erhöht die Punktzahl des Spielers um ds Punkte. PROCEDURE Load(VAR h : File); VIRTUAL; Lädt den Zustand des Spielers. PROCEDURE MovePlayerTo(where : PBasic); Bewegt (oder teleportiert) den Spieler zu der Position where, dieser Parameter muß ein Raum oder ein Objekt mit dem Attribut enterable_at sein. PROCEDURE SetState(s : BYTE); Verändert den Spielerstatus. SetState(dead_ps) tötet den Spieler, während ihn SetState(victory_ps) das Spiel gewinnen läßt. FUNCTION StatusBarLeftStr : STRING; VIRTUAL; Bestimmt den linksbündigen Text in der Statuszeile. FUNCTION StatusBarRightStr : STRING; VIRTUAL; Bestimmt den rechtsbündigen Text in der Statuszeile. PROCEDURE Store(VAR h : File); VIRTUAL; Speichert den Zustand des Spielers. FUNCTION RankStr : STRING; VIRTUAL; Gibt die Punktzahl und wahlweise weitere Informationen aus. PROCEDURE Victory; VIRTUAL; Wird nach dem Sieg des Spielers aufgerufen, um eine beliebige Meldung auszugeben. DESTRUCTOR Done; Löscht dynamische Variablen. 9.10 Methoden von TGame ----------------------- TGame beinhaltet den Parser und steuert den Spielverlauf. CONSTRUCTOR Init(_statusline,_upsize,_verbose : BOOLEAN; _history : BYTE); Initialisiert das Spiel (s. 4.5). FUNCTION ActorWhere : PRoom; Liefert den Aufenthaltsort des Akteurs. FUNCTION ActorInside(container : PItem) : BOOLEAN; Stellt fest, ob sich der Akteur in container befindet. PROCEDURE AddProlog(_str : STRING); Fügt dem Spiel einen Vorspann hinzu. Mit mehreren AddProlog() kann auch ein Text von mehr als 255 Zeichen zugewiesen werden. PROCEDURE AddVerb(_verb,_syntax : STRING; _event : BYTE); Fügt dem Wörterbuch ein neues Verb hinzu (s. 7.2). PROCEDURE Admonition(adress : TAdress); VIRTUAL; Fordert den Spieler auf, sich wieder auf das Spiel zu konzentrieren, wenn er eines der Ereignisse dance_ev, jump_ev und kiss_ev auslöst. Um auf diese Ereignisse anders zu reagieren, müssen Sie diese Methode oder einen Nachfolger von TBasic.HandleEvent() überschreiben. FUNCTION AfterAction(event : TEvent) : BOOLEAN; Wird von TGame.HandleEvent() nach Ausführung eines Ereignisses aufgerufen und verhindert eine Textausgabe, wenn False zurückgegeben wird. FUNCTION BeforeAction(event : TEvent) : BOOLEAN; Wird von TGame.HandleEvent() vor Ausführung eines Ereignisses aufgerufen und verhindert eine weitere Bearbeitung, wenn False zurückgegeben wird. PROCEDURE BeforeParsing(VAR input : STRING); VIRTUAL; Ermöglicht die Veränderung des Eingabestrings, bevor er an den Parser übergeben wird. PROCEDURE ClearDictionary; Löscht das gesamte Wörterbuch. Die Richtungsanweisungen (norden, süden, etc.) bleiben erhalten, da sie unabhängig vom Wörterbuch bearbeitet werden. PROCEDURE DelVerb(_event : BYTE); Entfernt alle Verben mit dem Ereignis _event aus dem Wörterbuch. FUNCTION GetActor : PItem; Liefert einen Zeiger auf den Akteur. Wenn NIL zurückgegeben wird, ist der Spieler der Akteur. PROCEDURE GetTime(VAR hour,min : BYTE); Liefert die Spielzeit. PROCEDURE HandleEvent(VAR event : TEvent); VIRTUAL; Wird von TBasic.HandleEvent() für die Ereignisse lista_ev, sleep_ev und wait_ev aufgerufen. FUNCTION MetaVerb(mv : BYTE) : BOOLEAN; VIRTUAL; Wird vor der Ausführung eines Metaverbs aufgerufen und kann dessen weitere Bearbeitung verhindern (s. 7.5). FUNCTION MyScope(_id : WORD; _action : BYTE) : BOOLEAN; VIRTUAL; Bestimmt während der Analyse der Eingabe, ob das vom Spieler genannte Objekt mit der ID _id das Ereignis _action ausführen kann. Solange Sie diese Methode nicht überschreiben, liefert sie immer True. Diese Methode wird aufgerufen, wenn Sie in einer Grammatik das Token routine angeben. PROCEDURE ParserError(n : BYTE; str : STRING; i : BYTE; VAR error : BOOLEAN); VIRTUAL; Gibt eine Meldung des Parsers aus, wenn bei der Analyse der Eingabe oder der Ausführung eines Ereignisses ein Problem auftritt. PROCEDURE Run; Startet das Spiel. PROCEDURE SetTime(_time,_rate : WORD); Bestimmt die Spielzeit in Minuten seit Mitternacht. Der Parameter _rate gibt an, wieviele Minuten während einer Runde verstreichen. DESTRUCTOR Done; Ruft am Programmende die Methode Done aller Nachfolger von TBasic auf. -------------------------------------------------------------- Einen Index finden Sie in der Postscript-Version dieses Textes