Zrobiłem sobie kilka dni przerwy, ale dziś wróciłem do projektu gry. I udało mi się w końcu dokończyć system walki.
Okazało się, że zdecydowanie więcej czasu niż samo programowanie zajęły mi prace koncepcyjne. W pewnym momencie zacząłem się zastanawiać, w którą stronę właściwie chcę poprowadzić mechanikę walki. Ogólny pomysł jest taki, żeby gra karciana miała trochę więcej wspólnego z klasycznymi grami strategicznymi — szczególnie z takimi tytułami jak Panzer General.
Jeżeli ktoś lubi stare gry strategiczne, to można nawet zagrać w Panzer General online: https://playclassic.games/games/turn-based-strategy-dos-games-online/play-panzer-general-online/
Jak ktoś lubi retro, zdecydowanie polecam. :)
Trochę inaczej niż w typowej karciance
Ale wracając do mojej gry. W większości gier karcianych mamy losową kolejność dobieranych kart, natomiast sama walka jest stosunkowo przewidywalna. U mnie, przynajmniej na ten moment, koncepcja jest trochę odwrotna.
Talię będziemy budować w taki sposób, żeby ustalić kolejność kart, które będziemy dobierać. Losowość pojawi się natomiast podczas samej walki. Nie oznacza to jednak, że wszystko będzie zależało od rzutu kostką — statystyki jednostek i ich wyposażenia mają mieć tutaj znacznie większe znaczenie.
Z Panzer General zaczerpnąłem między innymi podział jednostek na SOFT, HARD i AIR, a także ideę jednostek dystansowych. Do tego dorzuciłem własną mechanikę zasięgu, która określa, z której strefy możemy zaatakować przeciwnika.
Może najlepiej będzie pokazać na filmie, jak wygląda ekran podglądu walki:
A teraz po kolei. :)
Na powyższym podglądzie atakujemy czterema żołnierzami wyposażonymi w różne rodzaje broni. Zacznijmy od pierwszego.
I. Karabin
Poniżej pełna definicja karty:
cards["card_24"] = {
name_key = "card_24_name",
type = CardType.WEAPON,
faction = Faction.NEUTRAL,
cost_initiative = 2,
cost_production = 2,
allowed_zones = {},
allowed_attachments = {},
attack = {
[TargetsTypes.SOFT] = { dice = "d6[0,1,2,3,4,5]", count = 1 },
[TargetsTypes.HARD] = 0,
[TargetsTypes.AIR] = 0
},
attack_range = 1,
target_type = TargetsTypes.SOFT,
defense = { dice = "d8[1,2,3,4,5,6,7,8]", count = 2 },
image = "24.png",
frame_key = "01"
}
Najważniejsza dla walki jest oczywiście sekcja:
attack = {
[TargetsTypes.SOFT] = { dice = "d6[0,1,2,3,4,5]", count = 1 },
[TargetsTypes.HARD] = 0,
[TargetsTypes.AIR] = 0
},
attack określa rodzaj ataku. Karabin posiada wyłącznie atak SOFT, wykonywany jednym rzutem kością d6[0..5]. Nie może więc skutecznie atakować celów typu HARD ani AIR.
attack_range = 1,
attack_range określa zasięg ataku. Wartość 0 oznacza, że danym wyposażeniem można się wyłącznie bronić. Przy attack_range = 1 możemy zaatakować najbliższą zajętą strefę przeciwnika.
Przykładowo, jeśli przeciwnik ma jednostki w FRONT, SECOND i BACK, to przy zasięgu 1 możemy zaatakować tylko FRONT. Przy zasięgu 2 możemy zaatakować FRONT albo SECOND, a zasięg 3 pozwala zaatakować dowolną strefę.
Co ważne, puste strefy są pomijane. Jeżeli przeciwnik nie ma żadnej jednostki w FRONT, to przy attack_range = 1 możemy zaatakować jednostkę znajdującą się w SECOND.
Kolejny parametr:
target_type = TargetsTypes.SOFT,
target_type określa typ jednostki. W tym przypadku mamy jednostkę typu SOFT, więc żeby skutecznie ją zaatakować, potrzebujemy ataku typu SOFT.
defense = { dice = "d8[1,2,3,4,5,6,7,8]", count = 2 },
defense, czyli obrona. W przypadku karabinu są to dwa rzuty kością d8[1..8].
II. Artyleria
Artyleria robi się już trochę ciekawsza. Poniżej tylko parametry związane z walką:
attack = {
[TargetsTypes.SOFT] = { dice = "d6[0,1,2,3,4,5]", count = 1 },
[TargetsTypes.HARD] = { dice = "d4[0,1,2,3]", count = 1 },
[TargetsTypes.AIR] = 0
},
attack_range = 2,
is_ranged_attack = true,
prewent = true,
target_type = TargetsTypes.SOFT,
defense = { dice = "d6[1,2,3,4,5,6]", count = 1 },
W przeciwieństwie do karabinu artyleria ma również niewielki atak przeciwko celom HARD:
[TargetsTypes.HARD] = { dice = "d4[0,1,2,3]", count = 1 },
Ma też większy zasięg:
attack_range = 2,
oraz jest jednostką dystansową i powstrzymuje kontratak:
is_ranged_attack = true,
prewent = true,
Artyleria ma jednak znacznie słabszą obronę — tylko jeden rzut d6[1..6]:
defense = { dice = "d6[1,2,3,4,5,6]", count = 1 },
Przy okazji widać tutaj pewną ważną zasadę, którą przyjąłem przy projektowaniu systemu. Kostki używane do ataku zaczynają się od 0, natomiast kostki używane do obrony od 1.
Czyli przykładowo atak może wykonać d6[0..5], podczas gdy obrona korzysta z d6[1..6]. Dzięki temu rozkład wyników obu mechanik jest trochę inny.
III. Samolot typu CAS
Następny jest samolot CAS:
attack = {
[TargetsTypes.SOFT] = { dice = "d10[0,1,2,3,4,5,6,7,8,9]", count = 1 },
[TargetsTypes.HARD] = { dice = "d8[0,1,2,3,4,5,6,7]", count = 1 },
[TargetsTypes.AIR] = { dice = "d8[0,1,2,3,4,5,6,7]", count = 1 }
},
attack_range = 3,
target_type = TargetsTypes.AIR,
defense = { dice = "d6[1,2,3,4,5,6]", count = 3 },
Samoloty mają największy zasięg — attack_range = 3, czyli mogą zaatakować dowolną zajętą strefę przeciwnika.
Co ważne, samolot może atakować wszystkie trzy podstawowe typy celów: SOFT, HARD oraz AIR. Ma również całkiem mocną obronę — trzy rzuty d6[1..6].
Nie jest jednak nietykalny. Samolot można kontratakować, oczywiście pod warunkiem, że przeciwnik posiada jednostkę z wyposażeniem mającym atak typu AIR.
IV Czołg:
I na koniec czołg:
attack = {
[TargetsTypes.SOFT] = { dice = "d8[0,1,2,3,4,5,6,7]", count = 1 },
[TargetsTypes.HARD] = { dice = "d12[0,1,2,3,4,5,6,7,8,9,10,11]", count = 1 },
[TargetsTypes.AIR] = 0
},
attack_range = 1,
target_type = TargetsTypes.HARD,
defense = { dice = "d8[1,2,3,4,5,6,7,8]", count = 3 },
Tutaj chyba wszystko jest dość intuicyjne. Czołg jest jednostką HARD, ma mocny atak przeciwko HARD — d12[0..11] — oraz całkiem dobrą obronę, czyli trzy rzuty d8[1..8].
Jego słabą stroną jest natomiast zasięg. attack_range = 1 oznacza, że musi zaatakować najbliższą zajętą strefę.
Wszystko jest w Lua
Definicje wszystkich kart znajdują się w pliku:defines/cards.lua:
https://github.com/Lukasz-B-82/MyOpenCardGame/blob/main/defines/cards.lua
I tutaj jestem całkiem zadowolony z wyboru Lua.
Wydaje mi się, że jest to jeden z najprostszych sposobów na przygotowanie definicji, które później można łatwo edytować. Z Lua znam się przede wszystkim z gier Paradox Interactive, gdzie ogromna część gry jest w ten sposób konfigurowalna.
Dokładnie taki efekt chciałbym osiągnąć tutaj. Docelowo chciałbym, żeby można było praktycznie z marszu, bez znajomości całego kodu gry, edytować istniejące karty, dodawać nowe, zmieniać ich parametry, a być może nawet modyfikować część mechanik.
Czyli coś w rodzaju: „nie musisz być programistą, żeby grzebać w tej grze”. :)
No dobra, ale jak to właściwie działa?
W całym tym podglądzie najważniejsza jest sekcja z Symulacją ataku:
Na przykładzie powyżej atakujemy przeciwnika wyposażonego jedynie w pistolet. Nasza armia składa się natomiast z żołnierzy z karabinem, artylerią, samolotem i czołgiem.
Szansa powodzenia ataku wynosi aż 85,9%, a szansa, że stracimy jednego żołnierza, to zaledwie 0,4%.
Teraz rozważmy atak na żołnierza z karbinem:
Szansa powodzenia ataku spada do 73.4% a możliwość straty żołnierza z karabinem wzrasta do 2.6%.
Czyli już sama zmiana wyposażenia przeciwnika z pistoletu na karabin robi całkiem sporą różnicę.
A co z artylerią?
Tutaj sytuacja jest trochę zaskakująca — szansa powodzenia ataku jest nawet wyższa niż w przypadku żołnierza z karabinem.
I w końcu najgorszy możliwy scenariusz:
Atakujemy żołnierza wyposażonego w czołg.
No i tutaj już nie wygląda tak fajnie. Szansa powodzenia ataku spada do 28,1%, a ryzyko utraty jednostek rośnie do 6,8% w przypadku żołnierza z karabinem i 5,4% w przypadku żołnierza w czołgu.
I właśnie o taki efekt mi chodziło — wyposażenie i typ jednostki powinny mieć realne znaczenie. Nie chcę, żeby wynik walki sprowadzał się do prostego „rzuć kostką i zobacz, co wypadło”.
I tutaj dochodzimy do AI
Małe podsumowanie, bo muszę przyznać, że zabierając się za ten projekt, myślałem, że wszystko będzie szło znacznie szybciej dzięki temu, że korzystam z AI.
I pod względem programowania rzeczywiście idzie to całkiem szybko. Problemem nie jest napisanie kodu. Problemem jest wymyślenie mechaniki, a następnie sprawdzenie, czy ta mechanika ma w ogóle sens.
I tutaj AI nie pomaga już tak bardzo. :)
Kiedy poprosiłem o pomoc w zbalansowaniu systemu walki, najlepszym pomysłem, jaki dostałem, było w zasadzie… pomnożenie wyniku razy dwa. XD
Serio.
Za to tekst:
determinacja — ranny obrońca broni się zaciekle
rozbawił mnie chyba bardziej, niż powinien. XD
I to chyba dobrze pokazuje, gdzie obecnie znajduje się największa część pracy. Kod można wygenerować, poprawić, przetestować i znowu poprawić. Natomiast ktoś musi zdecydować, jak właściwie ta gra ma działać.
I tym kimś, nadal jestem ja. ;)
To jeszcze nie jest gra
Tak naprawdę tej gry jeszcze nikomu nie pokazywałem, bo na obecnym etapie trudno byłoby ją nazwać grywalną.
Na razie traktuję te wpisy trochę jak dokumentację tego, co wymyśliłem i jak poszczególne mechaniki działają. Przy okazji mam też coś, co można pokazać i czym można się pochwalić, kiedy projekt będzie już faktycznie grywalny.
A do tego jeszcze trochę brakuje.
Muszę dokończyć kilka elementów systemu walki, a przede wszystkim zrobić przeciwników sterowanych przez AI. Bo obecnie, żeby zagrać, muszę grać… sam ze sobą. :)
Na szczęście komputer raczej nie będzie miał nic przeciwko temu, żeby w końcu dostać własną armię.
Linki do poprzednich wpisów:
@fat.music/gra-karciana-2-filmik-repozytorium
@fat.music/mam-dosc-php-z-pomoca