Programování textových her v TADS 3, část 6. – NPC postavy

Co by to bylo za příběh, kdyby jeho kořením nebyly postavy? Bohužel v českých textových hrách se skutečná interaktivita u postav objevuje pomálu, protože její programování je komplikované. Častěji se setkáme s kompletně statickými postavami, které samy od sebe nic nedělají, jen stojí na místě a čekají na hráče, nebo dokonce s postavami zmíněnými jen v textu – přijdou, něco udělají a už jsou zase pryč, takže s nimi hráč nemůže nijak interagovat. U nedůležitých postav si někdy takové zjednodušení dovolit můžeme, ale ve většině příběhů bychom měli mít jednu, dvě klíčové postavy, které jsou motorem příběhu, a ty bychom měli udělat mnohem zajímavější.

Dynamické postavy

Čím může být postava zajímavá? V klasické fikci jde o originální a dobře propracovanou osobnost, vybroušený dialog atp. To samé platí i pro interaktivní fikci, ale navíc nám jde o onu interaktivitu – postavám bychom měli vdechnout život a snažit se, aby se chovaly realisticky, tj. aby se pohybovaly, prováděly různé věci a reagovaly na odkrývající se příběh. Snažíme se předstírat, že mají svou vůli a jsou si vědomy svého okolí.

Problém je v tom, že naprogramovat takovou postavu vyžaduje mnoho specializovaného kódu, který bude řešit všechny aspekty chování v nejrůznějších situacích, což se při přímočarém přístupu může snadno zvrhnout v obří hromadu nepřehledného ifelse větvení, ve kterém je snadné se utopit. Proto je programování postav v TADS navrženo tak, že v samotném objektu postavy píšeme jen minimum programového kódu a většinu chování definujeme prostřednictvím různých objektů vkládaných do postavy pomocí kontejnerové hierarchie (pomocí znaménka plus). Jsou to stavy postavy, témata konverzace, konverzační uzly a agendy. Toto uspořádání si klade za cíl zachovat přehlednost programování i u velmi složitých postav.

Přesto konceptů, které si v následujících kapitolách představíme, je poměrně dost a chvíli bude trvat, než vám všechny střípky zapadnou do sebe. Proto doporučuji se s programováním postav seznamovat postupně a každou část si nejprve vyzkoušet a osvojit, než se pustíte do dalších částí.

Actor

NPC postavy reprezentujeme některou ze tříd Actor, UntakeableActor či Person. Rozdíl mezi třídami je poměrně malý, první bychom použili pro menší postavy, jako třeba domácí mazlíčky, které může hráč vzít do rukou, druhou zase pro větší postavy, jako jsou velká zvířata. Nejčastěji asi použijeme poslední třídu, která má trochu upraveny knihovní hlášky, aby se lépe hodily pro humanoidní postavy.

Definice objektu postavy se podobá definicím předmětů, opět musíme dodat jméno, popisek, slovník, gramatický rod a všechno hezky vyskloňovat. Na rozdíl od předmětů do slovníku přidáme i pátý pád (oslovujeme, voláme) a v ukázce si všimněte i legračně působících \’s převzatých z angličtiny, které označují přídavná jména přivlastňovací. Parser tato slova chápe, že určují vlastnictví, např. >vem Borisův kafovak nebo >sedni si na kapitánovo křeslo.

commander: Person
    'ivan petronov/velitel' 'velitel' *1 @dome
    "Určitě je starší, pravděpodobně jako kapitán. Zdá se, že i v tomhle
        vesmírném zapadákově dbá na pořádek i disciplínu. Má přísný vzhled,
        vlasy ostříhané na ježka a čistý, pečlivě upravený pracovní overal. "

    gcName = 'velitele, veliteli, velitele, veliteli, velitelem'
    gcVocab = 'ivana ivanovi ivane ivanu ivanem petronova/petronove/petronovi/
        petronovem/velitele/veliteli/velitelovi/velitelem/velitelův\'s/
        velitelova\'s/velitelovo\'s'
;

ActorState

Objekt třídy ActorState představuje to, co postava momentálně dělá, tedy její fyzický stav, jako třeba že rozmlouvá s hráčem, řídí vesmírnou loď, vyřizuje papíry v kanceláři, obědvá, spí nebo že je postava naštvaná, protože ji hráč vzbudil. Prakticky všechny vlastnosti, které se týkají stavu, jsou naprogramovány v ActorState objektu místo v postavě samotné. Aktuální stav postavy je uložen ve vlastnosti curState postavy a chceme-li ho změnit, zavoláme metodu setCurState(state), které předáme nový stav jako parametr. Na začátku hry se zvolí ten stav, který je označen vlastností isInitState = true.

V objektu musíme definovat především dvě vlastnosti, které stav popisují. Vlastnost stateDesc se použije při prozkoumání postavy. Připojí se jako dovětek k základnímu popisu z objektu Actor, a proto se obvykle formuluje tak, aby na popis navazoval, tedy např. bez opakování jména postavy. Naproti tomu vlastnost specialDesc se použije při rozhlédnutí v místnosti a umístí se do samostatného odstavce, takže je potřeba popis náležitě formulovat.

+ commanderWelcomes: ActorState
    stateDesc = "Je plný nadšení z vašeho příletu. "
    specialDesc = "Velitel vás vítá a je plný nadšení z vašeho příletu. "
;

Kromě základní třídy ActorState máme k dispozici i několik specializovaných potomků, které se hodí pro specifické situace:

  • HermitActorState je stav postavy, která se nenechá od hráče vyrušit. Třeba proto, že spí, nevnímá nebo nechce být rušena, každopádně nereaguje na žádné konverzační příkazy hráče. V tomto stavu je vhodné kromě stateDesc a specialDesc (ty nastavujeme u všech stavů) nastavit také vlastnost noResponse na dvojitými uvozovkami ohraničený řetězec, který vysvětlí, proč postava nereaguje.
  • AccompanyingState je stav postavy, která automaticky doprovází hráče. Kamkoliv jde hráč, tam jde za ním i parťák.
  • GuidedTourState se podobá předchozímu stavu, ale liší se v tom, že NPC postava hráče vede a ukazuje mu směr dalšího pohybu. Nicméně i tak je na hráči, aby přesun zahájil buď příkazem směru nebo pomocí >následuj.

Konverzace

V textových hrách se experimentovalo s mnoha různými konverzačními systémy ve snaze najít ideál. Od asi nejstaršího systému ask/tell, kdy hráč může zadat téma, na které se chce zeptat nebo o něm postavě říci, přes pokusy o jednoduchou umělou inteligenci založenou většinou na rozpoznání určitých klíčových slov a ignorování struktury věty, přes konverzační menu, které je inspirováno grafickými hrami a ve kterém se dá konverzace libovolně větvit, přes klikání na hypertextové odkazy, kterými se dialog odvíjí dál a další, až po úplné zjednodušení na pouhé >mluv s postavou, kdy hráč zcela rezignuje na to, že by sám řídil směr, kterým se konverzace ubírá. Každý z těch systémů ale má nějaké nevýhody – ask/tell typicky vytváří bezstavové konverzace, umělá inteligence se chová hloupě, konverzační menu úplně vybočuje z běžného ovládání textové hry a to jednak režimem (když hráč vybírá směr komunikace v menu, nemůže zadat běžný příkaz) a jednak stylem uvažování (výběr v menu je čistě deduktivní činnost, odpadá to kouzlo neomezenosti) atp.

Mike Roberts si byl vědom problémů, které různé konverzační systémy přinášejí, a proto i když navrhl pro TADS konverzační systém založený na ask/tell, doplnil ho řadou různých vylepšení, která pomáhají odstraňovat jeho nevýhody a přináší některé možnosti, které mají větvené konverzace pomocí menu, a přitom dokonale zapadá do modelu ovládání hry příkazovou řádkou.

Témata konverzace

V systému založeném na ask/tell se může hráč ptát nebo postavě říkat o různých věcech. Má k tomu k dispozici příkazy >zeptej se kapitána na loď či >řekni veliteli o součástkách. Kromě nich může hráč postavě nějaký objekt ukázat či podat >ukaž modul Borisovi, >podej kafe Borisovi či si o něco naopak říci >požádej o identifikační kartu. Odpovědi na tyto příkazy programujeme jako objekty následujících tříd:

  • AskTopic odpovídá na příkaz >zeptej se na téma.
  • TellTopic odpovídá na příkaz >řekni o tématu.
  • AskTellTopic odpovídá společnou odpovědí na kterýkoliv ze dvou předchozích příkazů.
  • GiveTopic odpovídá na příkaz >dej předmět postavě.
  • ShowTopic odpovídá na příkaz >ukaž předmět postavě.
  • GiveShowTopic odpovídá společnou odpovědí na kterýkoliv ze dvou předchozích příkazů.
  • AskForTopic odpovídá na příkaz >požádej o předmět.
  • AskAboutForTopic odpovídá na kombinaci >zeptej se na předmět či >požádej o předmět.
  • AskTellAboutForTopic odpovídá na předchozí kombinaci doplněnou o >řekni o předmětu.
  • AskTellGiveShowTopic odpovídá na vše kromě žádosti o předmět.
  • AskTellShowTopic odpovídá na kombinaci zeptání, říkání a ukázání.

Jak vidíte, jsou k dispozici třídy kombinující různé konverzační příkazy, aby se omezily problémy s uhodnutím konverzačního příkazu. Např. většina odpovědí na otázky se pro jistotu programuje jako kombinovaný AskTellTopic, leda že by bylo potřeba u stejného tématu mít rozlišené ptaní a říkání. Na následujícím příkladu vidíte téma, kterým se velitele můžeme zeptat, co si myslí o Borisovi:

+ AskTellTopic @technician
    "<.p><q>Veliteli, mohu se vás zeptat?</q> kladeš otázku, <q>co je za
        člověka, ten váš technik?</q>

        <.p><q>Myslíš Borise?</q> obrátil se k tobě velitel, <q>odborník na svém
        místě, má znalosti i šikovné ruce, až na to jeho kafování -- bez pytlíku
        toho černýho vývaru nevydrží snad ani minutu.</q> "
;

Objekty témat se umisťují pomocí kontejnerové hierarchie (pomocí znaménka plus) buď přímo do NPC postavy, pak jsou k dispozici kdykoliv, nebo do některého ActorState objektu, pak jsou k dispozici jen v tom daném stavu postavy. Opět využijeme šablony, takže buď za zavináčem uvedeme jeden objekt, ke kterému se dialog vztahuje, nebo v hranatých závorkách uvedeme seznam několika objektů. Místo objektu či jako doplněk k vyjmenovaným objektům můžeme zadat i regulární výraz do jednoduchých uvozovek. Konverzační příkazy se mohou odvolávat na jakýkoliv objekt, který hráč už potkal a viděl, resp. jakýkoliv objekt označený isKnown = true. Text odpovědi se umístí buď do dvojitých uvozovek, nebo se použije některý z EventListů, o kterých jsme mluvili ve druhém článku v souvislosti s atmosférou hry. Několik příkladů jejich využití uvidíte v kapitole o výchozích odpovědích.

Při psaní konverzací nepište pouze přímou řeč odpovědi. Používejte vždy úplný dialog, kdy nejprve napíšete otázku, jak se postava zeptala, a pak celou odpověď. Nejen že to zabrání nedorozumění, ale v moderněji pojaté textové hře by postava hráče měla mít svou osobnost a tohle je příležitost, jak ji zviditelnit.

Alternativní odpovědi

Často se stává, že odpovědi u jednoho tématu je potřeba měnit podle nejrůznějších okolností. Např. když v Základně na asteroidu Boris hráči trpělivě radil s opravou modulu, bylo těch variant požehnaně (než hráč našel modul navigace, než ho otestoval, než našel druhý modul, než diagnostikoval porouchané součástky, než zjistil, že ve skladu jich není dostatek, než je předal veliteli a po té). Sice bychom mohli místo textu ve dvojitých uvozovkách deklarovat metodu topicResponse() a v jejím těle program rozvětvit mohutnou strukturou ifů či switchů, ale TADS se nás od takového způsobu programování snaží odstínit. Místo toho pomocí kontejnerové hierarchie do zadaného tématu přidáme jeden či více AltTopiců, což jsou odpovědi, které nahradí původní odpověď, jestliže je splněna jejich isActive pomínka. Níže si ukážeme kratší příklad, na kterém je mj. vidět společná reakce na více objektů. Když je AltTopiců více v řadě a více z nich má splněnu podmínku isActive, použije se ten poslední aktivní.

++ AskTellTopic [ coffeeBox, coffee1, coffee2, coffee3 ]
    "<.p><q>Ty bez toho kafe neuděláš ani krok,</q> usmál ses na Borise,
        <q>stojí vůbec za to?</q>

        <.p>Boris potáhl z kafovaku dlouhý lok: <q>Soustavně a marně se pokouším
        dostat sem pravou arabicu, ale v tom koncentrátu nebudou ani ty mletý
        bukvice, jak říkával můj děda. Ale máme jí hodně.</q> "
;

+++ AltTopic
    "<.p><q>Borisi, jsi v pořádku?</q> naklonil jsi lehce hlavu na bok, aby ti
        neunikl ani hlásek. <q>Nepřehnal jsi to s tím kafem?</q>

        <.p>Na Borisovu odpověď jsi nečekal dlouho: <q>Dej mi laskavě
        pokoj.</q> "

    isActive = technician.isIn(toilet)
;

Vlastnost isActive můžeme použít nejen u AltTopiců, ale i u hlavního tématu, pak ovlivňuje téma úplně celé vč. případných alternativních odpovědí.

Definice abstraktních témat

Nejčastěji se konverzace programují tak, že tématem, na který se může postava ptát, jsou konkrétní předměty ve hře. Když hráč najde rozbitý navigační modul a neví, co s ním, může zkusit příkaz >zeptej se na modul a Boris mu poradí. Je to vlastně zjednodušení konverzačního systému, aby hráč nekladl zcela abstraktní otázky, jako >zeptej se na smysl života apod., jejichž formulace by nebyla předvídatelná.

Přesto pokud ve hře chceme mít možnost se zeptat na nějaké téma, které neodpovídá žádnému hernímu objektu, a postaráme se, aby se hráč v příběhu dozvěděl o možnosti mluvit o tomto tématu, můžeme ho do hry přidat. Vytvoříme objekt třídy Topic, který definuje patřičný slovník. Pojmenujeme si ho a pak ho použijeme při definici tématu stejným způsobem, jako by se jednalo o běžný předmět.

tCargo: Topic 'náklad/nákladu';
tFuelConsumption: Topic 'spotřeba spotřebu spotřebě paliva/palivo/palivu';
tJourney: Topic 'cesta/cesty/cestě/cestu/cestou';
tService: Topic 'převzetí přebrání služby/službu/služba';

Napovídání témat

Jeden z hlavních rozdílů ask/tell systému oproti konverzačnímu menu spočívá v tom, že hráč nemá před sebou výběr témat, o kterých může hovořit. Takový způsob sice velice dobře zapadá do stylu ovládání příkazovým řádkem, podporuje dojem neomezenosti ovládání, o který moc stojíme, ale když si dobře neuvědomíme důsledky a nepostaráte se o dobré vedení hráče, může se snadno změnit v hlavní nevýhodu. TADS proto umožňuje napovídat témata konverzace:

>mluv s kapitánem
„Brýtro, kapitáne,“ pozdravil jsi rozespale.

Kapitán se k tobě otočil. „Á, konečně jsi vzhůru. Už jsem tě chtěl jít z toho pytle vysypat.“

(Mohl by ses ho zeptat na loď, na cestu, na asteroid, na náklad nebo na převzetí služby.)

Normálně se seznam doporučených témat objeví na začátku konverzace, když hráč použije příkaz >mluv s postavou, a když hráč požádá o připomenutí témat příkazem >témata. Když ale uznáme za vhodné, třeba dojde k důležitému posunu v příběhu a otevřou se nové možnosti, můžeme seznam doporučených témat hráči nabídnout vložením tagu <.topics> do vypisovaného textu. V ukázce k tomu dojde, když se hráč zeptá na inspekční cestu a Boris se při tom zmíní, že se Chrissovi něco stalo. To je důležitý posun v příběhu a chceme, aby hráč linii sledoval, proto mu hra nabídne: (Mohl by ses zeptat na Chrisse.) Také si všimněte, že při využití AltTopiců na rozdíl od větvení pomocí ifů, můžeme mít téma doporučené jen pokud se hráč o smrti Chrisse dozvěděl a jinak ne.

+ AskTellTopic @tInspection
    "<.p><q>Prosím tě, Borisi,</q> ztišil jsi hlas do tajuplného šepotu, <q>na
        jakou inspekci jezdí?</q>

        <.p>Technik k tobě pomalu stočil zrak: <q>Co já vím? Mně se tady toho
        moc neřekne! A potom, co se stalo s Chrissem, začal jezdit šéf po
        asteroidu na ty inspekce ještě častěji.</q>
        <.reveal chriss-death><.topics> "
;

+ AskTellTopic @tChriss
    "<.p><q>Borisi,</q> zeptal ses technika, <q>jaký byl vlastně ten
        Christian Anderson, ten Chriss?</q>

        <.p><q>No,…</q> Boris očividně s odpovědí nespěchá, <q>párkrát jsme se
        potkali tady na základně, ale on se moc s nikým nebavil, chtěl mít
        pečlivě vyloženo a naloženo, to ho zajímalo nejvíc.</q> "
;

++ AltTopic, SuggestedAskTopic
    "<.p><q>Ty, Borisi,</q> zeptal ses technika, <q>co se vlastně stalo s tím
        Chrissem?</q>

        <.p><q>Hmm, no,</q> Borisovi se do odpovědi očividně nechce, <q>taková
        blbá nehoda. Nevím, co ho to napadlo, vyrazil ven, že si vezme nějaké
        vzorky kamenů. Netuším, k čemu. A když se nehlásil, jeli ho velitel s
        kapitánem hledat. Našli ho v zakázané oblasti už mrtvýho. Viděl, jsem
        když ho přinesli. Rozsekl skafandr, zřejmě nějaké blbé došlápnutí,
        zakymácení -- náhoda a bylo to.</q><<gSetKnown(sq71)>> "

    name = 'Chrisse'
    isActive = gRevealed('chriss-death')
;

Téma doporučíme přidáním třídy SuggestedAskTopic (či Tell, Give, Show, AskFor) a každopádně každé doporučené téma musíme pojmenovat. Název (vlastnost name) se použije při vypisování seznamu doporučených témat. U otázek a také u podání, ukázání a požádání o objekt pojmenujeme téma ve čtvrtém pádu, protože bude dosazeno do věty „mohl by ses zeptat na koho/co“. U oznamovacích témat pojmenujeme téma v šestém pádu „říci o kom/čem“.

Napovídání témat není přímým ekvivalentem konverzačního menu. Předně je plně v rukou autora hry, která témata a kdy napoví a která nikoliv. Napovězená témata neslibují, že jsou kompletním výčtem všech možností. Můžete tedy napovědět jen to nejdůležitější a očividné pro vývoj příběhu, objevení bonusových témat můžete nechat na hráči. Nápovědy se také neobjevují vždy, ale hráč o ně musí požádat, což zpravidla hráč nebude dělat, pokud opravdu nepotřebuje pomoci.

Výchozí odpovědi

Vždy je dobré pokrýt co nejvíce témat, jak jen bude možné. V naší hře s dvěma tucty lokací a třemi postavami jsme měli asi dvě stovky konverzačních témat. Ale ať se budete snažit sebevíce, nikdy nedokážete pokrýt úplně všechno, o čem by mohl hráč chtít mluvit. Přesto by nebylo vhodné nechat postavu úplně mlčet, to by úplně zkazilo dojem z konverzace. Když už tedy neumíme odpovědět úplně na všechno, můžeme se alespoň snažit zachovat dojem konverzace definicí výchozích odpovědí, které zachytí neznámá témata.

Výchozí témata se definují podobně, jako běžná témata konverzace, jen pro jejich vytvoření se použije některá ze tříd DefaultAskTopic, DefaultTellTopic, DefaultAskForTopic, DefaultGiveTopic nebo DefaultShowTopic. Existují i kombinované třídy DefaultAskTellTopic a DefaultGiveShowTopic nebo i zcela obecná třída DefaultAnyTopic, které zachytí úplně všechny druhy konverzace. Pokud použijete konkrétní i obecnější kombinovanou třídou, konkrétní mají přednost.

+ DefaultAskTopic, ShuffledEventList
    [
        '<.p><q>Borisi,</q> nadechl ses, <q>mohl bych se ještě zeptat…?</q>
            <.p><q>Cože chceš?</q> Boris očividně bloumá ve svých myšlenkách,
            <q>nevím, asi 42.</q> ',
        '<.p><q>Jo, a Borisi,</q> začal jsi, ale technik se už předem brání:
            <.p><q>Nevím, netuším a nechci vědět.</q> ',
        '<.p>Pověděl jsi Borisovi svou otázku, ale ten jen odsekl:
            <.p><q>A jak bych to mohl vědět? Vypadám snad jako tetička
            Wiki?!</q> '
    ]
;

+ DefaultTellTopic
    "<.p><q>Borisi,</q> nadechl ses k dalším slovům, ale technik tě přerušil:
        <.p><q>Můžeš mě nechat aspoň chvíli napokoji?</q> "
;

+ DefaultAskForTopic
    "<.p><q>Borisi,</q> nadechl ses, <q>mohl bys mi půjčit <<gTopicText>>?</q>
        <.p>Ale technik nevypadá soustředěně: <q>Cože chceš? Nemám, nedám.</q> "
;

+ DefaultAnyTopic
    "<.p><q>Borisi,</q> oslovil jsi technika, ale ten tě očividně nevnímá:
        <.p><q>Hmm, to jsou mi věci, když to říkáš.</q> "
;

A protože tyto výchozí odpovědi uvidí hráč často, je dobré je variovat pomocí EventListů. Zkuste udělat odpovědi zábavné a využijte tuto příležitost k upevnění charakteru postavy. Čím barvitější postava, tím snazší to bude – víte, jaké možnosti v tomto ohledu skýtá třeba postava dobráckého, ale senilního čaroděje? Pokuste se ale formulovat výchozí odpovědi ne tak, že postava o tématu nic neví, to by pro řadu otázek znělo jako nesmysl, ale raději tak, že se postava o tématu nechce bavit. Třeba je moc zaneprázdněná a hráče nevnímá nebo pokud je postava nepřátelská, může odmítnout odpovědět jen z čiré škodolibosti a postavě hráče se jen vysmát.

Navázání a ukončení konverzace

Aby byla konverzace realističtější, je vhodné ji dát nějaký začátek a konec. Nemusí to platit úplně pro každou konverzaci, ale když postavu hráč ve hře potká, je přirozené, že ji nejprve osloví, než začne se svou otázkou. Vůbec to neznamená, že by hráč musel zadat příkaz >pozdrav Borise a >rozluč se, aby s ním mohl mluvit, pokud to neudělá, pozdravení se vloží automaticky před prvním konverzačním příkazem a rozloučení se automaticky vloží před odchodem z místnosti. Ve hře to může vypadat následovně:

Velín
Jsi ve velící místnosti, odsud se řídí těžba i všechny důležité činnosti související s provozem základny. Uprostřed je pohodlné pracoviště s velikým křeslem, velkými obrazovkami a gumovým popruhem na ruce, aby se operátor nevznášel od terminálu, když píše na klávesnici. Podél zadní stěny stojí několik racků nacpaných servery.

Technik sleduje na obrazovce záběry kamer a několik textových výpisů současně. Občas je různě přepíná. Evidentně mu nedělá potíže sledovat několik věcí současně.

>pozdrav Borise
„Borisi, promiň, že ruším,“ přiblížil ses k technikovi, „měl bys chvilku?“

Boris zvedl hlavu: „Copak potřebuješ, Tobiáši?“

>zeptej se na modul
„No, chtěl bych se zeptat,“ začínáš váhavě, „co s tím modulem?“

„To není zas tak velký problém,“ usmál se Boris, „vezmi si skafandr a vylez ven k lodi. Na přídi najdeš radarovou navigaci, vytáhneš ovládací modul a prověříš ho.“

>prozkoumej Borise
Technik Boris je o trochu starší než ty a vypadá zanedbaně. Věnuje se práci a zároveň s tebou hovoří.

>v
Boris obrátil svou pozornost zpět k obrazovkám.

Všimněte si, jak hra rozlišuje, zda technik pracuje nebo s postavou hráče hovoří. Používá se k tomu dvojice stavů ConversationReadyState a InConversationState, které se mezi sebou automaticky přepínají. Ve zdrojovém kódu musí být uspořádány následovně:

+ technicianTalking: InConversationState
    stateDesc = "Věnuje se práci a zároveň s tebou hovoří. "
    specialDesc = "Boris s tebou během práce hovoří. "
;

++ technicianWorking: ConversationReadyState
    stateDesc = "Zrovna sleduje obrazovky počítače a kamerového systému. "
    specialDesc = "Technik sleduje na obrazovce záběry kamer a několik textových
        výpisů současně. Občas je různě přepíná. Evidentně mu nedělá potíže
        sledovat několik věcí současně. "
;

+++ HelloTopic, StopEventList
    [
        '<.p><q>Borisi, promiň, že ruším,</q> přiblížil ses k technikovi,
            <q>měl bys chvilku?</q>
            <.p>Boris zvedl hlavu: <q>Copak potřebuješ, Tobiáši?</q> ',
        '<.p><q>Borisi?</q>, promluvil jsi na technika.
            <.p>Boris se otočil od práce k tobě: <q>Tak co ti zase nejde?</q> '
    ]
;

+++ ByeTopic, ShuffledEventList
    [
        '<.p><q>Díky moc, jdu pokračovat.</q>
            <.p>Boris obrátil svou pozornost zpět ke své práci. ',
        '<.p><q>Děkan, to mi pomohlo.</q>
            <.p>Boris obrátil svou pozornost zpět ke své práci. ',
        '<.p><q>Máš to u mě!</q>
            <.p>Boris obrátil svou pozornost zpět ke své práci. '
    ]
;

+++ ImpByeTopic, CyclicEventList
    [
        '<.p>Technik opět zaměřil svou pozornost na obrazovky počítačového
            terminálu, jakoby se jim nikdy věnovat nepřestal. ',
        '<.p>Boris obrátil svou pozornost zpět k obrazovkám. ',
        '<.p>Boris opět věnuje plnou pozornost obrazovkám. '
    ]
;

Konverzační témata, která mají platit jen v tomto stavu, vložíme se dvěma plus do InConversationState. Do ConversationReadyState umístíme speciální témata, která se použijí jako pozdravení a rozloučení. Všimněte si, jak u HelloTopic používáme StopEventList, aby první pozdrav byl jiný, než každý další. Rozloučení naopak střídáme nahodile, rozlišujeme však, zda hráč zadal příkaz rozloučení, nebo se rozloučení vložilo automaticky. Na výběr máme:

  • HelloTopic reaguje na příkaz >pozdrav postavu a použije se i tehdy, pokud hráč začne rozhovor bez příkazu k pozdravení a nemáme žádný ImpHelloTopic.
  • ImpHelloTopic vloží pozdrav do rozhovoru zahájeného hráčem bez příkazu pozdravení.
  • ActorHelloTopic je pozdrav, který se vloží do rozhovoru zahájeného NPC postavou, když sama hráče osloví.
  • ByeTopic reaguje na příkaz >rozluč se a zároveň vkládá rozloučení i do rozhovoru skončeného automaticky, pokud není definováno některé z následujících bye témat.
  • ImpByeTopic vkládá rozloučení do rozhovorů skončených automaticky bez příkazu k rozloučení, leda že by ho zastoupilo některé z konkrétnějších bye témat.
  • BoredByeTopic je rozloučení, které se použije, když postava ztratila trpělivost, protože si ní hráč určitý počet tahů nekomunikoval. Výchozí hodnota jsou 4 tahy a to lze změnit nastavením vlastnosti attentionSpan v InConversationState objektu.
  • LeaveByeTopic se použije, když hráč během rozhovoru odejde pryč do jiné lokace.
  • ActorByeTopic použije NPC postava, když sama rozhovor ukončí. Stačí, aby programátor zavolal metodu endConversation() dané postavy.
  • HelloGoodbyeTopic je společné téma, které se použije místo pozdravu i rozloučení.

Nekonverzační témata

U některých témat se vám může stát, že budete chtít místo otázky a odpovědi napsat jen nějakou vnitřní úvahu postavy hráče, proč se na takovou věc raději ptát nebude. Třeba by položení takové otázky bylo pro postavu hráče moc nebezpečné, příliš brzy by záporákovi prozradil, že ho z něčeho podezřívá atp. Taková témata označíme isConversational = nil, a tím dojde k vynechání případného pozdravení a rozloučení, protože vlastně nedošlo ke skutečné komunikaci mezi postavami.

Tuto techniku můžeme snadno přenést i na doporučená témata na pokračování, kdy zpravidla používáme StopEventList k postupnému odvíjení konverzace na stejné téma, přičemž poslední odpověď se pak opakuje dokola a shrnuje, co se hráč dozvěděl a už ho jen informuje, že téma vyčerpal. Pomocí timesToSuggest řekneme, kolikrát má být téma doporučeno, tedy přesněji kolik máme užitečných odpovědí. Všimněte si použití metody curiositySatisfied, která informuje o jejich vyčerpání.

++ AskTellTopic, SuggestedAskTopic, StopEventList [tCargo, tFuelConsumption]
    [
        '<.p><q>Myslel jsem, že letíme skoro s prázdnou,</q> řekl jsi s pohledem
            upřeným na obrazovky, <q>a podle tohoto ukazatele snad stěhujeme
            slona.</q>
            <.p>Kapitán poklepal na obrazovku a odfrkl si: <q>Zatracená
            kalibrace!</q> ',
        '<.p>Znovu jsi upřel pohled na ukazatele spotřeby: <q>Asi bych se na to
            měl na základně podívat. Ale v každém případě bychom to měli dát do
            záznamu, ať se na to podívají doma technici.</q>
            <.p><q>To neřeš,</q> zabručel kapitán, <q>tahle mašina to dělá při
            každé cestě, po každé opravě. Furt.</q> ',
        '<.p>Kapitánovi jsi o zvýšené spotřebě paliva už řekl, ale prý si s tím
            nemáš dělat žádné starosti. '
    ]

    name = 'náklad'
    timesToSuggest = 2
    isConversational = !curiositySatisfied
;

Samozřejmě pokud by i poslední odpověď byla formulovaná jako skutečná otázka vůči postavě a postava by na ní odpověděla třeba nějakou výmluvou, potom bychom sice použili timesToSuggest = 2, ale neoznačovali bychom poslední odpověď, že není konverzační.

Konverzační uzly

Konverzace se v ask/tell systému odehrává na přeskáčku, většinou nelze předpokládat, v jakém pořadí bude hráč komunikovat o konkrétních tématech. Někdy ale může nastat situace, že se konverzace ocitne v určitém bodu, kdy přirozeně vyvstane množina očekávaných reakcí, které by v jiném kontextu nedávaly smysl nebo měly jiný význam. NPC postava se třeba může hráče na něco zeptat a hráč bude chtít odpovědět >ano nebo >ne nebo prozradí něco klíčového pro vývoj příběhu a bude nasnadě reagovat určitým způsobem.

Tyto situace jsou v TADS modelovány pomocí konverzačních uzlů umístěných do NPC postavy. V následující ukázce vidíte začátek rozhovoru z naší hry, kdy hráč odjel vozítkem ze základny a velitel hráče volá vysílačkou. K programovému zahájení rozhovoru, kdy hráče osloví NPC postava, je určena metoda commander.initiateConversation(commanderOnRadio, 'do-you-read-me'); který postavu přepne do patřičného stavu a (volitelně) aktivuje určitý konverzační uzel. Ty jsou identifikovány textovým názvem, jehož volba je na nás. Typické schéma takového konverzačního uzlu vypadá následovně:

+ ConvNode 'do-you-read-me'
    npcGreetingMsg()
    {
        "<.p><q>Kchh prask chrchle chrapl…,</q> ozvalo se z vysílačky.
            <q>Základna volá průzkumné vozidlo Červ 2. Ohlaš se,</q> zní v rádiu
            velitel. A po chvíli znovu: <q>Tady velitel, volám Červa, ohlaš se.
            Mám signál, že jsi opustil základnu.</q><.reveal hint-avoid> ";
    }

    limitSuggestions = true
    isSticky = true

    canEndConversation(actor, reason) { return nil; }

    npcContinueList: ShuffledEventList
    {
        [
            '<q>Haló, Tobiáši, ozvi se,</q> opakuje velitelův hlas monotónně ve
                vysílačce, jakoby to byl stařičký gramofon se zaseknutou
                jehlou. ',
            '<q>Základna volá Červa! Červe, ozvi se,</q> a po chvíli znova:
                <q>základna volá Červa, Červe, ozvi se!</q> ',
            '<q>Tobiáši, potvrď příjem, Tobiáši, ozvi se,</q> slyšíš opakovat
                velitelův hlas ve vysílačce. ',
            'Ta vysílačka ti jde na nervy: To je pořád <q>Ozvi se! Haló! Slyšíš
                mě, Tobiáši?</q> '
        ]

        eventPercent = 67
    }

    commonFollowup = "<.p>Poznáváš velitelův značně podrážděný hlas: <q>Nemůžeš
        jen tak opustit základnu. Koukej se hned vrátit! Co tě to vlastně
        napadlo sebrat vozítko a odjet pryč?</q>

        <.p><q>Kruci a kruci,</q> pomyslel sis, takhle brzo jsi vyhmátnutí
        nečekal. Bude to chtít získat trochu času.<.convnode why-leaved> "
;

Metoda npcGreetingMsg zobrazí úvodní oslovení hráče. Konverzační uzel je normálně aktivní jen během jediného konverzačního příkazu a pokud hráč žádnou z možných odpovědí nezvolí a komunikuje o něčem jiném, je opuštěn. V našem příkladu jsme ale chtěli, aby se hráč musel ohlásit a musel konverzačním uzlem projít, proto jsme ho označili vlastností isSticky = true a takový uzel trvá až do té doby, než ho programově opustíme. Alternativní možností, jak udržet současný konverzační uzel aktivní, je umístit do textu odpovědi tag <.constay>.

Aby hráč věděl, že jeho odpověď stále očekáváme, zobrazují se mu hlášky z npcContinueList. Je vhodné je variovat a nezobrazovat je po úplně každém tahu, aby hra nevypadala mechanicky. Zároveň lze řídit, zda a za jakých okolností může hráč utéci z celého rozhovoru. Metoda canEndConversation má za úkol rozhodnout, zda hráč může ukončit rozhovor. My ho zde nedovolíme ukončit za žádných okolností, ale mohli bychom důvod testovat (rozloučení, znudění NPC postavy, odejití do jiné lokace) a rozhodnout podle okolností.

Hráč může v naší hře na volání reagovat různými způsoby, ale ve skutečnosti je to jen trik, protože na všechny v tomto okamžiku reagujeme ve výsledku stejně. Proto jsme si v konverzačním uzlu připravili metodu commonFollowup (to je na rozdíl od knihovních vlastností a metod zmíněných v předchozím textu čistě náš název), do které jsme umístili společné pokračování. V něm současný uzel opustíme a aktivujeme nový vypsáním tagu <.convnode why-leaved>. Změnit konverzační uzel můžeme i příkazem commander.setConvNode(node) a pokud bychom chtěli současný uzel jen opustit bez přepnutí do nového, použijeme nil.

Od hráče jsme očekávali, že odpoví buď >ano či >ne resp. že zkusí zahájit rozhovor příkazem >pozdrav velitele. Proto jsme do konverzačního uzlu umístili patřičná témata konverzace, ale jak vidíte, i jakékoliv jiné téma jsme pomocí DefaultAnyTopic proměnili v začátek rozhovoru:

++ HelloTopic
    "<.p>Po chvíli váhání odpovídáš: <q>Tady Tobiáš.</q>
        <<location.commonFollowup>> "
;

++ YesTopic
    "<.p>Po chvíli váhání odpovídáš: <q>Tady Tobiáš.</q>
        <<location.commonFollowup>> "
;

++ NoTopic
    "<.p>Pane kopilote, přestaňte nás ignorovat a okamžitě se ohlaste!
        <.p>Nemá smysl si dál hrát na hluchého a odpovídáš: <q>Tady Tobiáš.</q>
        <<location.commonFollowup>> "
;

++ DefaultAnyTopic
    "<.p>Něco nesrozumitelného jsi zahudral do vysílačky.
        <<location.commonFollowup>> "
;

SpecialTopic

Konverzační systém ask/tell sice skvěle zapadá do ovládání textové hry a oproti konverzačnímu menu podporuje pocit neomezenosti ovládání, působí ale trochu mechanicky – hráč se stále jen ptá nebo říká o objektech. Přitom do menu bychom mohli umístit mnohem pestřejší volby, které se ze schématu zcela vymykají, jako třeba „omluv se“, „lži“ apod. TADS tuto možnost doplňuje, ale v trochu jiné podobě, která se více hodí ke zbytku konverzace. Takovéto speciální možnosti, jak v rozhovoru pokračovat, prezentuje jako doporučená témata, která vypíše v závorce. Přitom hráče nenutí si některou z možností vybrat, může zadat jakýkoliv jiný konverzační i nekonverzační příkaz.

Protože se jedná o možnost mimo běžné schéma, musíme parseru poskytnout vyčerpávající sflovník, jak hráč může zareagovat. Jakákoliv odpověď hráče, která se skládá pouze ze slov vyjmenovaných v hranatých závorkách, se bere jako shoda.

++ SpecialTopic 'se ohlásit'
    ['ohlaš', 'ohlásit', 'ozvi', 'ozvat', 'odpověz', 'odpovědět', 'se', 'mu',
        'jim', 'veliteli', 'kapitánovi', 'základně']
    "<.p>Po chvíli váhání odpovídáš: <q>Tady Tobiáš.</q>
        <<location.commonFollowup>> "
;

++ SpecialTopic 'je ignorovat'
    ['ho', 'je', 'velitele', 'kapitána', 'ignoruj', 'ignorovat']
    "<.p>Pane kopilote, přestaňte nás ignorovat a okamžitě se ohlaste!
        <.p>Nemá smysl si dál hrát na hluchého a odpovídáš: <q>Tady Tobiáš.</q>
        <<location.commonFollowup>> "
;

Speciální témata nejsou nové příkazy k zapamatování, jsou to jednorázové možnosti, jak v rozhovoru pokračovat, které platí jen v ten okamžik, kdy je TADS nabídne. Proto se vždy umisťují do konverzačního uzlu a jsou automaticky doporučené. Ukázka navazuje na příklad z předchozí kapitoly o konverzačních uzlech.

Agenda

Třída AgendaItem představuje něco, co chce NPC postava za určitých podmínek udělat sama od sebe. Máme-li agendu aktivovanou, tak jakmile je isReady pravdivé, potom se spustí metoda invokeItem(). Na příkladu je ukázána agenda, která zařídí, aby velitel před hráčem ukryl papíry do trezoru a vzbudil v něm podezření. Protože se to stane jednorázově, agendu označíme za splněnou nastavením vlastnosti isDone.

+ commanderHidingAgenda: AgendaItem
    initiallyActive = true
    isReady = commander.curState == commanderPaperWork && commander.canSee(me)

    invokeItem()
    {
        "<.p>Pánové sebou trhli a kapitán rychle shrábl listiny rozložené po
            stole. Když jsi udělal krok k nim, velitel vstal, listiny vložil do
            trezoru a prudce bouchl jeho dvířky: <q>Co se děje, mladíku?</q> ";

        isDone = true;
        gReveal('hint-hiding');
    }
;

ConvAgendaItem

Je to speciální agenda, která se spustí, jakmile postava hráče vidí a když s ní to kolo hráč nemluvil o něčem jiném. Hodí se tedy k zahájení rozhovoru resp. nadnesení tématu ze strany NPC postavy. Občasná výměna rolí, kdy NPC postava osloví hráče a hráč na ní reaguje, pomáhá pocitu realističnosti.

TravelAgendaItem

Tato třída sice není součástí knihovny, ale můžeme si ji snadno naprogramovat. Zařídí pohyb postavy po stanovené trase:

class TravelAgendaItem: AgendaItem
    travelPath = nil
    invokeItem()
    {
        local actor = getActor;
        local path = nilToList(travelPath);
        local loc = actor.getOutermostRoom();
        local idx = path.indexOf(loc);

        if(idx && idx < path.length())
        {
            if(me.canSee(loc)) me.trackFollowInfo(actor, path[idx + 1], loc);
            actor.scriptedTravelTo(path[++idx]);
        }

        if(idx == nil || idx >= path.length())
        {
            isDone = true;
            destReached();
        }
    }
    destReached() { }
;

V naší hře jsme ji využili např. pro odchod kapitána z kokpitu po předání služby, který vidíte níže:

+ captainLeaving: HermitActorState
    sayDepartingThroughPassage(conn) { "Zaslechl jsi zabzučení vodomatu a o
        chvilku později klapnutí krytu kapitánovy kóje. "; }
    sayDepartingLocally(traveler, dest) { ""; }
;

+ captainWalkAgenda: TravelAgendaItem
    isReady = (captain.curState == captainLeaving)
    initiallyActive = true
    agendaOrder = 10
    travelPath = [cockpit, livingRoom, captainsCabin]
;

Komandování NPC postav

Příkazy hráče nemusí plnit jen postava hráče, ale může o ně požádat i NPC postavu. Ovládání je úplně stejné, jako u hlavního hrdiny, jen před příkaz doplníme oslovení postavy a čárku, např. >Sally, jdi na jih nebo >robote, seber cihlu. Je možné použít jakýkoliv herní příkaz, ale ve výchozím stavu jej postava neposlechne, pokud to nepovolíme vrácením true z metody obeyCommand(issuingActor, action) postavy nebo jejího stavu. Můžeme tedy akci zkontrolovat a povolit jen určité příkazy v určité situaci a můžeme i změnit metodu defaultCommandResponse(fromActor, topic) a upravit tak hlášku o odmítnutém uposlechnutí příkazu. Především ale budeme potřebovat doprovodit uposlechnutý příkaz vhodným textem a k tomu slouží témata třídy CommandTopic, která se používají podobně, jako běžná konverzační témata, jen tím objektem je akční třída, např. JumpAction.

Jak vidíte, TADS přistupuje k programování NPC postav velice komplexním způsobem a v rukou zkušeného autora je možné vytvořit opravdu efektní hry. Pojďte posunout české textovky kousek dál!

Další díly seriálu

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

Napsat komentář

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