Thumbnail Game Combat Tracker

Door: Thijs Zumbrink
15-01-2012 15:43

Nu TaZCrash zo'n beetje af is ben ik alvast aan het bedenken wat mijn volgende hobby projectje gaat worden. Ik heb in de tussentijd allerlei ver uiteenlopende ideëen gekregen en bedacht me dat een simpele Dungeons & Dragons combat applicatie wel leuk zou zijn. Een programmaatje waarmee de dungeon master initiatives, tijdgebonden effecten en cooldowns bij kan houden.

Ik zou nog tijden met TaZCrash door kunnen gaan maar dat zou niet echt veel zin hebben. Enerzijds heb ik het gevoel dat er niet meer zo veel toe te voegen is dat het spel significant verbetert, anderszijds wil ik iets nieuws leren, dus een nieuw project lijkt logisch. Pas als ik een hoop over game programming geleerd heb kan ik een project aangaan dat langer duurt. Daar komt wel bij dat ik de source code van TaZCrash wat zal opknappen en de laatste geplande features zal implementeren, zodat het een klaar product is en eventueel ook als portfolio game kan dienen.

Een D&D combat tracker is ook een vorm van game programming. Het kan gezien worden als de kern van een turn-based game. Een hele simpele kern. Ookal is het niet te spelen als game, het kan worden gebruikt voor offline D&D sessies, wat het dus weer met gaming verbindt. Een aantal doelen voor dit project:

• Het moet leerzaam zijn. Ik ga de ncurses library leren om de view te realiseren, ookal zal de view losgekoppeld worden van de kern voor modulariteit.
• Het moet vrij algemeen inzetbaar zijn, dus niet al te verbonden met een bepaald spelsysteem. In principe kan het gebruikt worden voor alle spellen waarbij initiative order en effecten/cooldowns voorkomen.
• De doelen worden van tevoren allemaal opgeschreven zodat het project een duidelijke eindstreep heeft. Tijdens het project kunnen nog wel features bedacht worden maar in grote lijnen is het van tevoren al duidelijk.
• Niet al te groot, want ik moet ook nog met mijn studie door, en wil na dit project natuurlijk alweer allemaal nieuwe dingen proberen.

Een paar ideëen

Een van de aspecten van dit programma is de representatie van data. Hoe ga ik om met nieuwe effecten en nieuwe monsters in de huidige lijn van volgorde? Ik zit te denken aan een linked list die in volgorde afgelopen wordt. Elk effect en elke beurt van een karakter is dan een element in die list.

Bijvoorbeeld:
Alric heeft initiative 17
Goblin 1 heeft initiative 12 (zijn bonus is +6)
Fithos heeft initiative 12 (zijn bonus is +5)

De initiative order is nu:
(Begin) -> (17 Alric) -> (12.6 Goblin 1) -> (12.5 Fithos) -> (End)

Het eind kan vervolgens aan het begin gekoppeld worden zodat we een eeuwige loop hebben, en we via een pointer naar het element wijzen dat momenteel aan de beurt is.

In principe kunnen de initiative getallen nu weggelaten worden! Die getallen hebben eigenlijk als enig nut om op het begin van de combat een volgorde te bepalen, en dienen daarna alleen nog maar als referentie om het voor ons menselijk brein handig te houden. Als je de Pathfinder regels leest over het manipuleren van initiatives dan worden er zeer veel woorden besteed aan het doorvoeren van dit concept. Die woorden zijn slechts nodig om de getallen weer te laten kloppen, zonder getallen zou dat een stuk simpeler zijn naar mijn mening!

De representatie zou nu zijn:
-> (Alric) -> (Goblin 1) -> (Fithos) -
/ \
\________________________________________/

Maar voor het gemak schrijf ik:
(Alric) -> (Goblin 1) -> (Fithos) -> ...

Vervolgens is het tijd om na te denken over effecten met een bepaalde duur. Bijvoorbeeld, de Goblin schiet een giftige pijl op Fithos waardoor hij drie beurten vergiftigd is. Het moment van vergiftigen is de beurt van de Goblin, dus de initiative-volgorde zal drie beurten doorlopen worden en in de beurt van de Goblin houdt het weer op. Dit betekent dat Fithos drie maal aan de beurt komt terwijl hij vergiftigd is, en dus drie maal de negatieve effecten ervan ondervindt. Voor redenen die later duidelijk worden voegen we een nieuw element toe in de linked list na de Goblin, die de vergiftiging representeert:

(Alric) -> (Goblin 1) -> (Fithos poison) -> (Fithos) -> ...

Het nieuwe element houdt een stukje data bij, namelijk het aantal beurten dat het nog geldt. Dit aantal beurten begint op drie, en elke keer dat het element langs komt tijdens het aflopen van de volgorde, wordt het met één verlaagd. Bij nul verdwijnt het element zonder zijn effect uit te oefenen.

De vraag is nu: hoe representeer ik deze informatie op het scherm in de applicatie? Idealiter, wanneer een DM dit programma gebruikt, gaat het als volgt:
Message: Alric is aan de beurt
DM drukt op enter
Message: Goblin 1 is aan de beurt
DM drukt op enter
Message: Fithos ondervindt poison effect
DM drukt op enter
Message: Fithos is aan de beurt
Huidige effecten: poison

De vermelding van huidige effecten in Fithos' beurt is belangrijk! Niet eens omdat het nou zo belangrijk is dat die poison geldt, want dat heeft zijn effect al uitgeoefend vlak voordat Fithos aan de beurt kwam. Echter, wat gebeurt er wanneer we passieve effecten hebben? Denk aan een armor class buff die niet actief aan de beurt hoeft te zijn om een effect uit te oefenen, maar waarvoor het heel belangrijk is dat het vermeld wordt bij de huidige effecten!

Dit leidt tot een onderscheid van effecten: actief en passief. Beide effecten dienen bij "huidige effecten" vermeld te worden als ze op die persoon van toepassing zijn. Alleen actieve effecten dienen een eigen beurt te krijgen waarbij de DM op de hoogte wordt gesteld dat er actie ondernomen moet worden (de DM leest dat er een poison 'tick' is, rolt damage, en vertelt Fithos zijn ongelukkige lot.)

Wat gebeurt er als een karakter besluit van initiative te veranderen? Simpel! Hij wordt tijdelijk uit de roulatie gehaald. Doordat effecten aparte elementen in de linked list zijn, zullen de effecten gewoon doorlopen. Op die manier kan je niet aan je poison ontkomen, en zullen ook buffs aflopen als je te lang wacht. Het karakter kan vervolgens inspringen in de initiative order wanneer hij wilt. Wederom: in de Pathfinder boeken worden dit soort handelingen uitgelegd aan de hand van de initiative getallen, maar het hebben een veel intuitiever beeld van de bedoeling wanneer we die getallen vergeten en gewoon een volgorde hanteren. Een voorbeeld:
(Alric) -> (Goblin 1) -> (Fithos poison) -> (Fithos) -> ...

...
Message: Fithos ondervindt poison effect
DM drukt op enter
Message: Fithos is aan de beurt
Huidige effecten: poison
(Fithos denkt: hmm, het is handiger als Alric eerst aan de beurt is)
Fithos zegt: delay initiative!
DM drukt op "delay"

De initiative order ziet er nu zo uit:
(Dat hoeft niet weergegeven te worden op het scherm)
Active: (Alric) -> (Goblin 1) -> (Fithos poison) -> ...
Suspended: (Fithos)

Message: Alric is aan de beurt
(Alric slaat bijvoorbeeld de Goblin)
Fithos zegt: ik wil na Alric aan de beurt zijn.
DM drukt op "insert delayed"

De initiative order ziet er nu zo uit:
(Alric) -> (Fithos) -> (Goblin 1) -> (Fithos poison) -> ...

Message: Fithos is aan de beurt

Op deze manier kan er niet gecheat worden met initiatives en duur van effecten, en is het delayen van initiative geimplementeerd zoals dit door Pathfinder bedoeld is.

Code voor karakters en effecten

De representatie van huidige effecten op een karakter kan simpelweg gerealiseerd worden door een lijst van effecten bij te houden in het karakter-element. Andersom kan een lijst van karakters bijgehouden worden in het effect-element. In pseudocode:
Character::addEffect (Effect e) {
current_effects.add(e)
}

Character::removeEffect (Effect e) {
current_effects.remove(e)
}

Character::showEffects () {
Output current_effects
}

Character::tick () {
Show message about this character
this.showEffects()
}

Effect::create (list of Characters cs, integer d) {
foreach Character c in cs {
c.addEffect(this)
}
current_chars <- cs
duration <- d
}

Effect::expire () {
foreach Character c in current_chars {
c.removeEffect(this)
}
Notify the initiative-order that this effect should be
removed. It will be removed from the linked list and
its predecessor and successor will be linked
(Doubly-linked list needed?)
}

Effect::tick () {
duration <- duration - 1
if duration = 0 {
this.expire()
} else {
Show message about this effect
}
}

Die code is in een notendop wat nodig is. Daar komen nog details bij kijken zoals strings die aangeven hoe een karakter precies heet of hoe een effect genoemd wordt, maar dat is triviaal. De code voor het evalueren van de initiative order en het delayen van initiative is een ander verhaal, welk denkwerk ik overlaat voor wanneer ik echt met het project begin. Het zou niet moeilijk moeten zijn, in principe een implementatie van een linked list met toevoeg- en verwijdermethoden. Vervolgens komt er een basisklasse "Node" met abstracte functie "tick", waar de klassen Character en Effect van afstammen. (En de klasse ActiveEffect stamt weer af van Effect, het verschil zit hem in dat ActiveEffect een bericht geeft wanneer het aan de beurt is.)

Ik heb nu al meer uitgewerkt dan ik van plan was, het is gevaarlijk verslavend om hiermee bezig te zijn! Daarom brei ik maar snel een eind aan dit artikel, want ik moet nog beginnen met het stellen van de uiteindelijke doelen!

Reacties
Log in of registreer om reacties te plaatsen.
Thijs Zumbrink, 15-01-2012 19:04:
De code van Effect::tick moet iets aangepast worden om de timing goed te laten lopen aan de hand van de duur van een effect:
Effect::tick () {
Show message about this effect
duration <- duration - 1
if duration = 0 {
this.expire()
}
}

Anders zou het effect een beurt te kort duren! Dus de vergiftigde pijl met een duur van drie beurten evalueert als volgt:
• Aangemaakt, duration=3
• Tick, show message, duration=2
• Tick, show message, duration=1
• Tick, show message, duration=0, expire
Zo wordt het bericht correct drie maal getoond.