Programování textových her v TADS 3, část 5. – Akce

Je to trochu zvláštní, že už jsme se naučili vytvořit rozličné místnosti a objekty, a přitom jsme zatím ještě nenaprogramovali téměř žádný kód. Doposud jsme si vystačili převážně s deklaracemi různých objektů a jejich vlastností, tvořili jsme data, ale doopravdy jsme nenaprogramovali skoro ani jeden příkaz, ani jednu funkci či metodu. Je to proto, že TADS nám poskytuje bohatou knihovnu implementující základní model textovkového světa a běžné chování běžných objektů už máme připravené. Teď tomu bude ale jinak. Je čas naučit se programovat reakce na chování hráče, které skutečně budou tvořit fungující hru, ve které o něco jde.

Akce hráče můžeme rozdělit na akce bez použití objektu (např. >západ, >vyskoč nebo >křič), akce používající jen tzv. přímý objekt (např. >vem klíč, kde akce manipuluje s jediným objektem ale i >sněz párek a rohlík, kde se sice mluví o dvou objektech, ale oba jsou ve stejné roli, s oběma se stane to samé) a akce zahrnující přímý a nepřímý objekt (např. >polož hrneček na stůl nebo >uřízni provaz nožem), kde akce ovlivňuje přímý objekt a nepřímý objekt je v roli pomůcky či cíle takové akce. Přímé objekty akce se ve zdrojovém kódu označují jako dobj a nepřímé objekty jako iobj. Kromě toho existují ještě tématické akce, kde místo objektu se vyskytuje téma, jako jsou konverzační akce apod. Seznam všech akcí naleznete v TADS 3 Library Reference Manual.

Akce bez objektu

Akce, které se neváží k žádnému objektu, se programují tak, že se přetíží metoda execAction patřičné akční třídy. V ní typicky provedeme nějaké kontroly, abychom věděli, v jaké situaci hráč příkaz použil (např. v jaké je místnosti, co má v inventáři apod.) a podle toho zareagujeme. Určitě je potřeba nezapomenout na zavolání zděděného chování klíčovým slovem inherited, pokud vysloveně nechceme standardní chování knihovny potlačit.

modify JumpAction
    execAction()
    {
        if(me.isIn(tunnel))
        {
            "Vyskočil jsi do výšky, lehce jsi plácnul dlaněmi v rukavicích do
                stropu, odrazil ses a zas jsi plynule přistál. ";
        }
        else if(me.isIn(supplyRoom))
        {
            "Vyskočil jsi do výšky a přistál jsi na pracovním stole. ";
            me.moveIntoForTravel(srTable);
            srTable.hasBeenStoodOn = true;
        }
        else inherited;
    }
;

Akce s objekty

Kód reagující na akce hráče, které manipulují s objekty, se programuje právě v definicích těchto objektů. Chceme-li reagovat na akci, v níž je objekt přímým objektem akce, zapíšeme reakci do dobjFor(Název akce) { … }. U akcí používající přímý i nepřímý objekt, jako např. >polož hrnek na stůl, můžeme reakci naprogramovat buď jako dobjFor(PutIn) objektu hrnku, který je přímým objektem manipulace nebo iobjFor(PutIn) objektu stůl, který je nepřímým objektem manipulace, tedy cílem položení. Obvykle si buď můžeme vybrat, kde je reakce vhodnější, nebo pokud chceme potlačit či změnit standardní chování, řídíme se tím, jak je akce naprogramována v knihovně.

V rámci dobjFor resp. iobjFor se pak uvádí jedna či více funkcí (popř. seznam PreCondition), které představují různá stádia zpracování akce. Níže je naznačena reakce na akci >sněz jahody, což je akce Eat. Názvy všech akcí najdeme TADS 3 Library Reference Manual.

+++ strawberries: Food 'jahody*jídlo oběd' 'jahody' *3
    "Jahody jsou spíš menší a vypadají lehce nedozrále, ale v hydroponii toho
        asi moc vypěstovat nejde. Energie ani náhradních výbojek Sunmaster není
        nikdy nadbytek. "

    dobjFor(Eat)
    {
        remap() { ... }
        preCond = [ ... ]
        verify() { ... }
        check() { ... }
        action() { ... }
    }
    isPlural = true
    changeGender = 'jídl:4, oběd:2'
    gcName = 'jahod, jahodám, jahody, jahodách, jahodami'
    gcVocab = 'jahod/jahodám/jahodách/jahodami*jídla*jídlu*jídlem'
;

Řada triviálních reakcí je už v knihovně předprogramovaná. Např. reakcí na sebrání předmětu je jeho přesunutí do inventáře hráčovy postavy. Někdy stačí tak málo, jako zobrazit upravenou hlášku doprovázející sebrání, jindy budeme chtít sebrání zabránit nebo naprogramovat komplet novou logiku akce. Musíme proto porozumět tomu, jak vykonání akce probíhá a jakými stádii program postupně prochází.

Remap

Ještě dříve, než se začne provádět akce definovaná na objektu našeho zájmu, je možnost vykonání akce přesměrovat někam jinam na jiný objekt. Změnit jednu akci na jinou. Kvalitní textové hry mívají poměrně hlubokou implementaci a složitější předměty bývají často složené z více objektů, které lze samostatně prozkoumat nebo jimi manipulovat. Tak třeba trezor v naší hře má samostatný objekt dvířek a ty mají samostatné objekty pro kliku a číselník. Hráč pak může zadat příkaz >otevři trezor, ale pokud bude chvíli zkoumat a hrát si, určitě ho napadne také >otevři dvířka, >pohni klikou nebo >zmáčkni kliku. Zkrátka existuje více logických způsobů, jak trezor otevřít a nechceme-li hráče frustrovat, musíme pochopitelně na všechny možné způsoby reagovat.

V ukázce níže vidíte, jak všechny myslitelné akce vztahující se k objektu kliky trezoru jsou přesměrovány na prostou akci >otevři trezor, kde se teprve ukrývá logika zámku trezoru, která buď dovolí a nebo nedovolí dvířka otevřít.

+++ Button, Component 'klika' 'klika' *3
    "Po zadání správné sekvence čísel se zmáčknutím kliky dvířka otevřou.
        Nebyla-li kombinace čísel správná, zmáčknutí kliky kombinaci zruší a
        může být zadána znovu. "

    dobjFor(Push) remapTo(Open, tresor)
    dobjFor(Pull) remapTo(Open, tresor)
    dobjFor(Move) remapTo(Open, tresor)
    dobjFor(Switch) remapTo(Open, tresor)
    dobjFor(Flip) remapTo(Open, tresor)

    gcName = 'kliky, klice, kliku, klice, klikou'
    gcVocab = 'kliky/klice/kliku/klikou'
;

Kromě bezpodmínečného přesměrování můžeme využít i podmíněné. Třeba níže prozkoumání dvířek si necháme jen pro otevřená dvířka a když jsou zavřená, přesměrujeme prozkoumání dvířek na prozkoumání celého trezoru (je zabudovaný ve stěně, takže nic kromě jeho čelní stěny nevidíme).

++ tresorDoor: ContainerDoor 'dvířka trezoru/dvířka' 'dvířka trezoru' *3
    "Dvířka trezoru mají vnější tlustou desku chránící mechaniku zámku ukrytou
        pod dalším krytem na vnitřní straně dvířek, ocelové závory jsou zasunuté
        uvnitř. "

    dobjFor(Examine) maybeRemapTo(!tresor.subContainer.isOpen, Examine, tresor)
    iobjFor(AttachTo) remapTo(AttachTo, DirectObject, tresor)

    isPlural = true
    gcName = 'dvířek trezoru, dvířkám trezoru, dvířka trezoru, dvířkách trezoru,
        dvířkama trezoru'
    gcVocab = 'dvířek dvířkám dvířkách dvířkami'
;

U akcí zahrnující přímý i nepřímý objekt, jako je AttachTo, použijeme při přesměrování symbol DirectObject resp. IndirectObject místo druhého resp. třetího parametru makra remapTo. Tím předáme do cílové akce ten druhý objekt, než u kterého jsme reakci napsali, tedy v reakci iobjFor předáváme DirectObject a naopak.

Jestliže chceme u jednoho objektu reagovat stejným způsobem na více akcí (více sloves), použijeme místo remapTo jednodušší makro asDobjFor resp. asIobjFor. V ukázce níže jsme zatím ještě vynechali konkrétní implementaci dobjFor(StandOn), povšimněte si však akcí Enter a ClimbUp, jak jsou učiněny synonymem k akci StandOn.

+ treadmill: Platform, Heavy, OnOffControl
    'posuvný běžecký pás' 'běžecký pás' *2
    "Běžná pomůcka pro udržování kondice posádky kosmických lodí. Má posuvný
        pás, přítlačné popruhy a pultík s ovládáním. "

    dobjFor(StandOn) { ... }
    dobjFor(Enter) asDobjFor(StandOn)
    dobjFor(ClimbUp) asDobjFor(StandOn)

    gcName = 'běžeckého pásu, běžeckému pásu, běžecký pás, běžeckém pásu,
        běžeckým pásem'
    gcVocab = 'posuvného posuvnému posuvném, posuvným běžeckého běžeckému
        běžeckém běžeckým pásu/pásem'
;

Verify

Hlavním účelem verify je posoudit, jestli a jak moc je vykonání určité akce logické. Pomáhá tím parseru rozhodnout se, které objekty jsou vhodnými cíli určité akce. Tak například máme-li ve hře dvoje dveře a jedny z nich jsou zavřené a druhé otevřené, pokud hráč zadá příkaz >otevři dveře, parser se rozhodne, že je logičtější otevřít zavřené dveře a nebude se hráče zbytečně ptát, které ze dvojice dveří má na mysli. Mluvíme-li o logičnosti akce, máme na mysli výhradně logičnost z pohledu hráče s jeho znalostmi příběhu, nikoliv logičnost z pohledu autora hry.

Nejjednodušším příkladem použití verify je deklarace s prázdným tělem funkce. Neužitečnost takové deklarace je jen zdánlivá, tímto způsobem totiž povolíme akci, která by jinak byla zakázaná. Tím, že jsme úmyslně nepoužili klíčové slovo inherited, tak jsme vyřadili z činnosti zděděné chování, které by akci zakázalo. Většina objektů má povoleny jen akce typické pro danou třídu objektu. Např. tlačítka lze mačkat, za páky lze zatáhnout, ale když chceme, aby se ručičkami plastové figurky (které jsou třídy Component, tedy samostatný detail většího objektu) dalo otáčet, musíme takovou možnost povolit.

+++ Component 'ručičky/nožičky/končetiny' 'končetiny figurky' *3
    "Tak jako u jiných plastových figurek se dají ručičky a nožičky polohovat
        otáčním. "

    dobjFor(Turn)
    {
        verify() { }
        action()
        {
            "Zkusil jsi o kousek pohnout končetinami figurky a za zvuku efektů
                <q>per huba</q> jsi zkusil figurkou pochodovat po stole…
                <q>Vžůůům, hurá na Měsíc,</q> mávl jsi figurkou v ruce v širokém
                oblouku. ";
        }
    }
    dobjFor(Move) asDobjFor(Turn)
    dobjFor(Push) asDobjFor(Turn)
    dobjFor(Pull) asDobjFor(Turn)

    isPlural = true
    gcName = 'končetin figurky, končetinám figurky, končetiny figurky,
        končetinách figurky, končetinám figurky'
    gcVocab = 'ručiček/ručičkám/ručičkách/ručičkami/nožiček/nožičkám/nožičkách/
        nožičkami/končetin/končetinám/končetinách'
;

Chceme-li naopak nějakou akci zakázat (na základě logičnosti z pohledu hráče), vložíme do těla verify funkce makro illogical se zprávou vysvětlující, proč akce nemohla proběhnout. Zprávu nikdy nezobrazujeme přímo ani nepoužíváme jiné příkazy, které by jakkoliv měnily stav hry. Parser totiž funkci verify volá nepředvídatelně na nejrůznějších objektech a často i opakovaně během jediné akce, takže jakékoliv vedlejší efekty by byly silně nežádoucí.

+ Wearable 'digitální náramkové hodinky' 'hodinky' *3
    "Prastaré digitální hodinky z doby, kdy displeje bývaly monochromatické a
        zobrazovaly pouze čísla. "

    dobjFor(Open)
    {
        verify() { illogical('Na jejich otevření bys musel mít nějaké hodinářské
            nástroje a i kdyby, tak je spíš rozbiješ, než vylepšíš. '); }
    }
    dobjFor(LookIn) asDobjFor(Open)

    isPlural = true
    gcName = 'hodinek, hodinkám, hodinky, hodinkách, hodinkami'
    gcVocab = 'digitálních digitálním digitálními náramkových náramkovým
        náramkovými hodinek/hodinkám/hodinkách/hodinkami'
;

Pro jemnější rozlišení míry nelogičnosti určitých akcí nabízí verify několik různých maker pro vysvětlení důvodu odmítnutí akce.

  • illogical(msg) odmítne akci, protože objekt pro tuto akci není v žádném případě vhodný.
  • illogicalAlready(msg) odmítne akci, protože už byla provedena. Nelze např. otevřít otevřené dveře.
  • illogicalNow(msg) odmítne akci, protože v daném okamžiku či stavu hry nedává smysl. Např. nemůžeme jíst, když máme na hlavě přilbu vesmírného skafandru.
  • illogicalSelf(msg) odmítne akci, kde jeden objekt má něco udělat sám se sebou, např. >uřízni pilku pilkou.
  • inaccessible(msg) odmítne akci s objektem, který vidíme a přesto na něj nedosáhneme, představme si třeba lustr vysoko na stropě.
  • dangerous povolí akci, ale jen když na ní hráč explicitně trvá, jinak o ní neuvažuje. Zabraňuje tedy tomu, aby se objekt zvolil automaticky za cíl akce, když hráč neřekne konkrétně, že má tento objekt na mysli. Hodí se třeba pro lahvičku s jedem, kterou bychom nechtěli rozbít, když hráč řekne jen >rozbij a neřekne co.
  • nonObvious funguje podobně, jako dangerous, ale používá se k zabránění vyřešení nějakého puzzlu omylem. Automatické akce dokážou být velmi nápomocné a určitě bychom nechtěli, aby hra automaticky zvolila sponku jako nástroj k odemknutí zámku jen proto, že hráč nemá žádný klíč a zadal příkaz >odemkni zámek aniž řekl čím.

Pochopitelně často budeme chtít volání illogical podmínit nějakým testem. Nechceme-li úmyslně potlačit testy logičnosti v knihovně, nesmíme zapomenout zavolat zděděné chování pomocí klíčového slova inherited. Vůbec nevadí, když my či knihovna vyprodukujeme více volání makra illogical, prostě se uplatní to s nejvyšší mírou nelogičnosti.

Check

Podobně jako verify i check slouží k zakázání akce. Na rozdíl od verify ale nijak neovlivňuje rozhodování parseru, a tak slouží jen a pouze k zakázání akce z pohledu a vůle autora hry. Všimněte si, že už nemluvíme o logičnosti z pohledu hráče. Ačkoliv z pohledu hráče je perfektně logické vložit identifikační kartu do čtečky a my jako autoři hry mu chceme umožnit, aby se o to pokusil, přesto nechceme, aby to vedlo k výsledku.

++ RestrictedContainer, Component
    'čtečka slot (na) (kartu)/čtečka/slot' 'čtečka na kartu' *3
    "Signalizuje stav zamčeno. "

    validContents = [card]
    cannotPutInMsg(obj)
    {
        gMessageParams(obj);
        return 'Čtečka je určena jen na identifikační karty, {kohoco obj} do
            ní vložit nemůžeš. ';
    }
    iobjFor(PutIn)
    {
        check()
        {
            inherited;
            failCheck('Zkusil jsi protáhnout kartu čtečkou, ale ta jen odmítavě
                zapípala. S touto kartou tyto dveře neotevřeš. ');
        }
    }

    gcName = 'čtečky na kartu, čtečce na kartu, čtečku na kartu,
        čtečce na kartu, čtečkou na kartu'
    gcVocab = 'čtečky čtečce čtečku čtečkou slotu slotem čtečky/čtečce/čtečku/
        čtečkou/slotu/slotem'
;

Všimněte si, že ani zde nezapomínáme volat inherited, aby i knihovna mohla provést své testy. Makro failCheck kromě zobrazení zprávy také ihned ukončí zpracování akce neúspěchem.

Action

Jestliže zpracování prošlo přes všechna přesměrování, nebylo zastaveno ani testem logičnosti ve verify, ani autor hry nerozhodl, že akce nemá proběhnout z jiných důvodů v check, potom hurá, konečně můžeme akci provést. Metoda action má za úkol provést změny v herním světě a popsat, co se stalo.

Někdy to může být snadné a stačí jen zobrazit nějakou vhodnou zprávu. Kdybychom to neudělali, použije se standardní zpráva z knihovny, která je však hodně obecná a trochu nudná. Každopádně nezapomeneme v takové situaci volat inherited, který např. v tomto případě zařídí, že objekt snězením zmizí ze hry. Samozřejmě pokud bychom měli jiný záměr a věděli, že nechceme použít standardní chování, které je pro jídlo naprogramováno (třeba bude hráči trvat snězení déle a pokaždé jen trochu ukousne), nemusíme inherited volat, ale musíme si pak zařídit vše sami.

+++ strawberries: Food 'jahody*jídlo oběd' 'jahody' *3
    "Jahody jsou spíš menší a vypadají lehce nedozrále, ale v hydroponii toho
        asi moc vypěstovat nejde. Energie ani náhradních výbojek Sunmaster není
        nikdy nadbytek. "

    dobjFor(Eat)
    {
        action()
        {
            "Snědl jsi jahody. Úžasné, jahody vypěstované na hydroponii na
                asteroidu byly úplně stejné jako ty, které ti babička kupovala v
                prosinci v Tescu. ";
            inherited;
        }
    }

    isPlural = true
    changeGender = 'jídl:4, oběd:2'
    gcName = 'jahod, jahodám, jahody, jahodách, jahodami'
    gcVocab = 'jahod/jahodám/jahodách/jahodami*jídla*jídlu*jídlem'
;

Jindy toho budeme chtít udělat více, například při odjetí vozítkem ze základny musíme zavřít dveře, načasovat různé události atp., zkrátka cokoliv je potřeba.

++ roverStick: Component 'řídicí páka/knipl' 'řídicí páka' *3
    "Řídicí pákou posádka může plynule měnit směr pohybu vozítka, potřebuje-li
        ho ovládat ručně. "

    dobjFor(Push)
    {
        verify() {}
        check()
        {
            if(!card.isIn(slot)) failCheck('Vůbec nic se nestalo. Jízda je
                zřejmě možná jen se zasunutou identifikační kartou ve slotu. ');
            inherited;
        }
        action()
        {
            "Přitáhl jsi řídicí páku k sobě a vyrazil na cestu.
                <.p>Razantní přitažení páky sice nezpůsobilo zaskřípění
                pneumatik a okamžité zrychlení v oblacích prachu, místo toho se
                vozítko rozjelo díky své vlastní automatice jen velmi pozvolna a
                opatrně, ale přesto ti teď už nic nemůže stát v cestě k
                tajemství na konci cesty. ";

            roverDoor.makeOpen(nil);
            roverDoor.makeLocked(true);
            rover.startRadioFuse();
        }
    }
    dobjFor(Pull) asDobjFor(Push)
    dobjFor(Move) asDobjFor(Push)

    gcName = 'řídicí páku, řídicí páce, řídicí páku, řídicí páce, řídicí pákou'
    gcVocab = 'páku/páce/pákou/kniplu/kniplem'
;

Kromě toho lze v tomto stádiu i použít makra nestedAction pro vyvolání nějaké samostatné akce jako nutné podakce té současné či replaceAction k přesměrování na jinou akci, které se od remapTo liší v tom, že se odehrává až po všech verify a check, ale to je již nad rámec tohoto článku.

PreCondition

V textových hrách, než můžeme něco podstatného udělat, je často potřeba vykonat řadu triviálních akcí, které jsou nezbytnými předpoklady žádané akce. Než sníme jablko, musíme ho sebrat, než projdeme dveřmi, musíme je otevřít, než dveře otevřeme, musíme je odemknout atp. Setkáváme se s tím i v běžných českých hrách, ale ve hrách naprogramovaných ve větších systémech, které simulují herní svět do větších detailů, je těch případů ještě mnohem více. TADS se snaží s těmito triviálními akcemi pomoci pomocí tzv. PreCondition, které nejen dokáží zkontrolovat, zda hráč může všechny potřebné akce udělat, ale i je za hráče automaticky provést. Ve hře to může vypadat následovně:

>dej tričko do levé přihrádky
(nejprve bereš bavlněné tričko, potom otevíráš levou přihrádku)

Dáváš bavlněné tričko do levé přihrádky.

>dej hodinky do levé přihrádky
(nejprve zkoušíš si sundat hodinky)

Raději ne, snadno by se ztratily, kdybys je neměl.

Knihovna nabízí a sama využívá řadu připravených PreCondition a samozřejmě lze naprogramovat další, ale to už by bylo nad rámec článku. Pojďme se raději podívat, jak je použít. V příkladu ohřívače jídla níže jich uvádíme hned několik. První z nich zařídí, že když hráč otevírá nebo zavírá ohřívač, nestojí při tom na běžeckém pásu (který je v opačném koutě místnosti). To zařizuje PreCondition nazvaná actorDirectlyInRoom a jak níže vidíte, přidáváme ji k případným zděděným podmínkám. Naproti tomu akce zapnutí ohřívače z knihovny nic nedědí, takže jen definujeme seznam svých podmínek. A ano, může jich být více, kromě té už zmíněné máme ještě jednu nazvanou objClosed, která zajistí zavření dvířek před zapnutím ohřevu.

++ owen: ComplexContainer, Component
    'mikrovlnný ohřívač/(jídla)' 'ohřívač jídla' *2
    "Ohřívač patří k nejdůležitějším přístrojům kosmického stravování.
        Elektromagnetickým zářením v oblasti mikrovln zahřívá obsah
        potravinových balíčků na poživatelnou teplotu. Vedle dvířek je jediný
        zapínací knoflík. "

    subContainer: ComplexComponent, OpenableContainer
    {
        maxSingleBulk = 15
        material = glass

        dobjFor(Open)
        {
            preCond = static inherited + actorDirectlyInRoom
        }
        dobjFor(Close)
        {
            preCond = static inherited + actorDirectlyInRoom
        }
    }
    dobjFor(TurnOn)
    {
        preCond = [actorDirectlyInRoom, objClosed]
        verify() {}
        check()
        {
            if(gRevealed('ship-malfunction') && !gRevealed('ship-landed'))
                failCheck('Zkusil jsi zapnout ohřívač, ale nestalo se vůbec
                    nic. Očividně ho taky postihl výpadek napájení. ');
            if(!tube.isIn(subContainer)) failCheck('Nejprve musíš do ohřívače
                vložit pochutinu k ohřátí. ');
            if(tube.eated) failCheck('Tuba už je úplně prázdná, není co
                ohřát. ');
        }
        action()
        {
            "Zapnul jsi ohřívač, který nejprve rozpoznal vložené potraviny a
                podle toho správně zvolil režim ohřívání. Zvučné cinknutí za
                chvilku oznámilo dokončení procesu. ";

            tube.heated = true;
        }
    }

    gcName = 'ohřívače jídla, ohřívači jídla, ohřívač jídla, ohřívači jídla,
        ohřívačem jídla'
    gcVocab = 'mikrovlnného mikrovlnnému mikrovlnném mikrovlnným ohřívače/
        ohřívači/ohřívačem'
;

Kompletní nabídku PreCondition i podrobnější vysvětlení tvorby nových podmínek najdete v TADS 3 Technical Manual.

Držení stavu hry

Pozorní čtenáři si už určitě v některých příkladech všimli zvláštních maker gReveal a gRevealed. Ty nám pomáhají udržovat si přehled o stavu, ve kterém se hra momentálně nachází. Jednoduše řečeno si pomocí nich můžeme označovat události či okolnosti, které se už staly. Už se hráč nasnídal? Už se loď porouchala? Už se hráč dozvěděl o smrti Chrisse? Podobné otázky si musíme klást v nejrůznějších situacích, jako např. když programujeme verify a check metody různých akcí, zpřístupňujeme konverzační témata nebo nápovědy.

Když k určité události dojde (např. v action metodě), můžeme si takovou událost označit zavoláním gReveal('nějaký název'). Přitom název, kterým událost označíme, je čistě na nás. Později lze kdykoliv testovat, zda událost nastala, pomocí druhého makra gRevealed('nějaký název') nejčastěji v podmínce nějakého if. V příkladu s ohřívačem jídla výše je takový test ukázán v check metodě. Ohřívač nejde zapnout, pokud už nastala porucha lodi (ship-malfunction), ale zároveň jsme ještě loď neopravili a nepřistáli (ship-landed).

Nové akce

Zavést úplně nové akce není složité. Nejprve pomocí makra vytvoříme akční třídu. Pro akce s přímým objektem na ukázce níže použijeme makro DefineTAction, kdybychom chtěli akci zahrnující i nepřímé objekty, použili bychom DefineTIAction a konečně akce bez objektů se zavádějí makrem DefineAction. Naši akci jsme pojmenovali Repair, vzniká tedy akční třída RepairAction. Potom musíme zavést jednu (nebo i více) gramatik do parseru pomocí VerbRule. Ta popisuje frázi, kterou hráč může použít. Každé slovo (slovesa, předložky) dáváme samostatně do jednoduchých uvozovek, na místě přímého objektu použijeme symbol singleDobj (nebo dobjList, když jich může být více najednou) a podobně pro nepřímé objekty. Svislítka v gramatice značí alternativní možnosti a kulaté závorky tvoří skupinu, která buď omezuje, kam až alternativa dosahuje, pokud jsou svislítka uvnitř, nebo celá tato skupina představuje alternativu, pokud je svislítko vně závorek. Vlastnost verbPhrase slouží jako šablona zpráv o dané akci. Podrobnější vysvětlení českého formátu verbPhrase najdete v dokumentaci k překladu TADSu.

DefineTAction(Repair);

VerbRule(Repair) ('oprav' | 'opravit' | 'sprav' | 'spravit') singleDobj
    : RepairAction
    verbPhrase = 'opravit/opravu{ješ}/opravil{a} (co)'
;

modify Thing
    dobjFor(Repair)
    {
        preCond = [touchObj]
        verify() { illogical('Budeš muset říci přesněji, jak chceš {kohoco dobj}
            opravit. '); }
    }
;

Tato akce byla triviální, v podstatě jen odchytávala pokus o opravení jakéhokoliv objektu vysvětlením, že hráč musí přesněji říci, co chce udělat. Kdybychom programovali skutečnou akci, definovali bychom na těch objektech, se kterými má akce fungovat, konkrétní chování. Podrobnější informace o tvorbě nových akcí naleznete v článku How to Create Verbs v technickém manuálu.

Také si všimněte, že ve verbPhrase i ve verify jsou použity zvláštní kousky textu obalené ve složených závorkách. Jsou to parametry, které do zpráv vkládají různé věci, jako názvy objektů, koncovky pravidelných sloves, vyskloňovaná zájmena a další texty, které se musí měnit podle okolností. Knihovna standardních hlášek je parametry doslova napěchovaná, protože její zprávy musí být univerzální a přizpůsobovat se objektům, o kterých mluví. Přizpůsobují se např. rodu, číslu, ale knihovna má zprávy parametrizované až tak, že dokáže vyprávět děj v 1.-3. osobě přítomného i minulého času. Při programování samotné hry se s nimi v takové míře nesetkáte, ale právě univerzální chybová zpráva nové akce je typickým příkladem využití. Podrobnější vysvětlení parametrů zpráv najdete v dokumentaci českého překladu.

Předzpracování příkazu

Někdy můžete narazit na situaci, která opravdu nezapadá do modelu, jak parser chápe příkazy hráče. V naší hře jsme měli situaci, kdy hráč musel vzít Borisovi identifikační kartu pomocí podávátka na zapadlé šroubky, které prostrčil skrz mřížku vzduchotechniky. Problém je v tom, že je to poměrně složitá situace, kterou každý hráč může chápat jinak. Proto jsme museli reagovat na různé příkazy. Naučili jsme hru např. >strč podávátko do mřížky, což je jedno z mnoha synonym akce PutIn, čili položení objektu do jiného objektu. Samozřejmě nedošlo k odložení podávátka, hra reagovala podáním si karty skrz mřížku. Synonym bylo mnohem více, dokonce stačilo i jednoduché >vem kartu, aby to hráč měl co nejjednodušší. Pochopitelně musel mít podávátko v inventáři.

Zjistili jsme však, že hráči mají tendenci takhle jednoduché řešení přehlédnout, prostě očekávají, že musí zadat nějaký složitější příkaz, ani na okamžik je nenapadne, že by to mohlo být takhle jednoduché. Nejtypičtěji zkoušeli >vem kartu podávátkem nebo >podej si kartu. To je ale pro parser docela velký oříšek. Tak předně akce Take je akce pracující jen s přímým objektem, nedovoluje určit nepřímý objekt vystupující v roli pomůcky. Sloveso „podej“ zase představuje akci GiveTo, čili předání předmětu NPC postavě, přičemž „si“ pochopí parser jako cílovou postavu. Aby hráč mohl někomu něco podat, musí to nejprve držet, proto se implicitně provede akce Take, to je dobře, puzzle je vyřešen, ale podivně pak působí zpráva „podáním karty sám sobě ničeho nedosáhneš“, když se parser snaží provést samotné GiveTo.

Šlo by zavést do celé hry novou akci TakeWith dovolující říci pomůcku, šlo by nějak potlačit nechtěnou chybovou zprávu GiveTo, ale bylo by to řešení zbytečně složité a dost možná i křehké, protože by ovlivňovalo celou hru. Přitom tohle byla opravdu jednorázová situace, kdy nestálo za to řešit celou věc nějak globálně. Proto jsme využili možnosti přepsat příkaz hráče ještě dříve, než se k jeho zpracování dostane parser. Tuto činnost vykonávají objekty třídy StringPreParser, které ve své metodě doParsing(str, which) mohou řetězec s příkazem hráče přepsat do jiné podoby. V příkladu vidíte, že používáme sílu regulárních výrazů, abychom zachytili, zda hráč zadal některou z forem příkazu, které by parser špatně pochopil, a takový příkaz změnili na pouhé „vem kartu“.

StringPreParser
    doParsing(str, which)
    {
        if(rexMatch(pat1, str) != nil || rexMatch(pat2, str) != nil
            || rexMatch(pat3, str) != nil) str = 'vem kartu';

        return str;
    }

    pat1 = static new RexPattern(
        '<NoCase>^(vem|vz[íi]t|podej|podat) +(si +)?kartu +pod[áa]v[áa]tkem')
    pat2 = static new RexPattern(
        '<NoCase>^(vem|vz[íi]t|podej|podat) +(si +)?pod[áa]v[áa]tkem +kartu')
    pat3 = static new RexPattern(
        '<NoCase>^(podej|podat) +(si +)?kartu')
;

Časování událostí

V textových hrách se celkem běžně stává, že je potřeba načasovat událost, aby se stala po určitém počtu tahů. Např. v Základně jsme chtěli, aby chvíli po tom, co Boris odvede hráče do skladu, vyrazil kapitán s velitelem směrem do kajuty. Událost jsme tedy načasovali tři tahy po vstupu do skladu.

Jednorázové události se časují pomocí třídy Fuse a opakované úplně stejným způsobem, jen se použije třída Daemon. V ukázce níže vidíte, že jsme ve skladu využili metody enteringRoom jako záchytného bodu, který nás informuje o vstupu postavy do místnosti. Přitom testujeme, zda se jedná o vstup hráče, zda kapitán stále hovoří s velitelem nahoře v kupoli a zda událost stále ještě nebyla načasovaná.

Příkazem new Fuse událost načasujeme. První dva parametry říkají, jaká metoda se má zavolat po skončení odpočtu. První parametr je objekt, na kterém má být metoda zavolána, v tomto případě klíčové slovo self říká, že na stejném objektu, kde událost časujeme a druhý parametr (nezapomeňte na znak ampersand) je ukazatelem na konkrétní metodu v daném objektu. Třetí parametr je pak počet tahů, které mají před spuštěním události uplynout.

Objekt vzniklý voláním new Fuse si musíme někam uložit, proto jsme si na objektu deklarovali vlastnost captainFuse. To je důležité, protože podle toho umíme poznat, že událost už byla načasovaná a pokud bychom chtěli, mohli bychom událost stornovat před jejím vykonáním, stačilo by zavolat captainFuse.removeEvent() a následně captainFuse = nil. To typicky potřebujeme hlavně u opakovaných událostí.

supplyRoom: Room 'Sklad' 'do skladu' 'ze skladu'
    "Sklad je docela rozlehlý, ale většinu prostoru zabírají regály s
        nejrůznějšími balíky obsahující vybavení a náhradní díly. V jednom
        z rohů je vyhrazené místo pro opravy s pracovním stolem a diagnostickým
        přístrojem a poblíž vchodu stojí bíle natřená lékárna. "

    captainFuse = nil
    captainWalk()
    {
        captain.setCurState(captainGoToCommandersRoom);
        commander.setCurState(commanderGoToCommandersRoom);
    }
    enteringRoom(traveler)
    {
        if(traveler == me && captain.curState == captainChatting
            && captainFuse == nil)
        {
            captainFuse = new Fuse(self, &captainWalk, 3);
        }
    }
;

Kromě událostí časovaných na určitý počet tahů disponuje TADS i událostmi v reálném čase. Pro jejich vytvoření se používají třídy RealTimeFuse a RealTimeDaemon, kterým se jako třetí parametr konstruktoru předává místo počtu tahů čas v milisekundách.

Další díly seriálu

Mohlo by se vám líbit...

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.