Files
rrttm/doc/CsataFormatum.md
markojozsef 54bc6a733a
Some checks failed
Build Windows EXE / build (push) Has been cancelled
Normális csata formátum dokumentáció
2026-10-20 17:28:58 +02:00

403 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Csataadatok és csatafüggvények technikai leírása
Ez a dokumentum a csatamód (`battle` állapot) forráskódja alapján írja le, hogyan kell felépíteni a `battle_data.battles` bejegyzéseit, és milyen argumentumokkal hívódnak az **MI-függvények**, a **képességek/tárgyak függvényei** és az **akcióanimációk**.
> **Jelölések:** Ami a csatamód forrásából közvetlenül kiolvasható, azt tényként írom le. Ami következtetés, vagy a nem látott fájlokban (`battle.py`, `battle_data.py`, `menu.py`, `text.py`) dől el, azt **[Feltételezés]** vagy **TODO** jelzi.
---
## 1. Terminológia
A kód belső nevei eltérnek a játékbeli fogalmaktól, ezért érdemes tisztán tartani:
| Kód | Játékbeli jelentés | Képernyőpozíció |
|---|---|---|
| `party_members` | A játékos által irányított karakter(ek), vagyis a megszállt főellenfél (és kísérői) | Bal oldal |
| `enemies` | A **hősök**, akik ellen a játékos harcol | Jobb oldal |
| `enemy_turn_func` | A hősök **MI-függvénye** | |
| `battle_id` | A csata azonosítója (a négy csataszoborhoz tartozik) | |
> **TODO:** Melyik `battle_id` melyik szoborhoz tartozik, és ki állítja be az értékét a csata előtt (feltehetően az overworld).
---
## 2. A csata betöltése
A `switch(game)` a `battle_data.battles[battle_id]` bejegyzésből tölti be a csatát. A következő mezők **mély másolatot** (`copy.deepcopy`) kapnak, tehát a csata közbeni módosításaik **nem maradnak meg** a következő csatára, és az újraindítás mindig tiszta állapotot ad:
`enemies`, `party_members`, `enemy_turn_func`, `init_ai_data`, `menus`, `spells`, `inventory`.
A sprite-ok, a háttér és a zene **nem** másolódnak, csak hivatkozás történik rájuk.
> **Fontos:** Mivel a másolat készül, az MI-függvények és képességfüggvények mindig a nekik **átadott** objektumokkal dolgozzanak, ne a `battle_data` modulban lévő eredetikre hivatkozzanak (pl. egy zárványon (closure) keresztül). Különben az eredeti, nem a csatában résztvevő példány módosulna. A `damage_callback` is csak olyan célponttal ad helyes pozíciót, amely a **jelenlegi** `enemies` vagy `party_members` listában van, különben a szám a `(0, 0)` pozícióban jelenik meg.
---
## 3. A `battle_data.battles[battle_id]` szerkezete
Egy bejegyzés egy szótár (dict) a következő kulcsokkal:
| Kulcs | Típus | Leírás |
|---|---|---|
| `"enemies"` | `list[Combatant]` | A hősök listája |
| `"party_members"` | `list[Combatant]` | A játékos által irányított karakterek listája |
| `"enemy_turn_func"` | függvény | A hősök MI-függvénye (lásd 8. fejezet) |
| `"init_ai_data"` | másolható objektum (általában `dict`) | Az MI kezdeti állapota, a csata elején másolódik |
| `"menus"` | `list[list[tuple[str, str]]]` | Karakterenkénti parancsmenük (lásd 5. fejezet) |
| `"spells"` | `list[list[tuple]]` | Karakterenkénti képességlisták (lásd 6. fejezet) |
| `"inventory"` | `list[list]` | A csatában használható tárgyak, **közösek** (lásd 7. fejezet) |
| `"enemy_sprites"` | `list[list[azonosító]]` | Hősönként 3 sprite-azonosító (lásd 11. fejezet) |
| `"party_sprites"` | `list[list[azonosító]]` | Karakterenként 3 sprite-azonosító |
| `"background"` | azonosító | Kulcs az `assets.sprites` szótárba |
| `"mus_id"` | azonosító | Kulcs az `assets.sounds` szótárba, a zene ismétlődve (`loops=True`) szól |
A `menus`, `spells`, `party_sprites` listák **indexe a `party_members` indexével egyezik**. Az `enemy_sprites` indexe az `enemies` indexével egyezik.
A csatát a `battle_data.battles[battle_id]` alapján választja ki a rendszer. A `battles` tehát olyan tároló, amely a `battle_id` értékkel indexelhető.
### Váz példa
```python
battles = [
{
"enemies": [Combatant(...), Combatant(...)],
"party_members": [Combatant(...)],
"enemy_turn_func": hero_ai_1,
"init_ai_data": {"turn": 0},
"menus": [
[("Támadás", "phys_att"), ("Varázslat", "magic"), ("Tárgyak", "items")],
],
"spells": [
[("Tűzgolyó", 5, ("Tűzgolyó", fireball, "ene"))],
],
"inventory": [
["Gyógyital", 3, ("Gyógyital", use_potion, "ally")],
],
"enemy_sprites": [
["hero1_normal", "hero1_hurt", "hero1_ko"],
["hero2_normal", "hero2_hurt", "hero2_ko"],
],
"party_sprites": [
["boss1_normal", "boss1_hurt", "boss1_ko"],
],
"background": "bg_arena_1",
"mus_id": "mus_battle_1",
},
# ... további 3 csata
]
```
---
## 4. `Combatant`
A `Combatant` osztály (`battle.py`) konstruktora **nem látható** a csatamód forrásában, ezért itt csak az onnan elérhető attribútumokat sorolom fel:
| Attribútum | Használat a csatamódban |
|---|---|
| `name` | Megjelenített név (menü, célpontválasztó) |
| `hp` | Aktuális életerő (célpontválasztóban kijelzett; sprite-váltás) |
| `maxhp` | Maximális életerő. A 10%-os sprite-váltás küszöbe |
| `ko` | Legyőzött-e (a szereplő nem cselekszik, a célpontválasztóból kimarad) |
| `stone` | Kővé változott-e (nem cselekszik, szürke sprite) |
| `def_up` | Védekezik-e (a célpontválasztóban „Védés” felirat) |
| `atb` | Az akcióig hátralévő időzítő (lásd 9. és 12. fejezet) |
| `attack` | Támadóerő, a „Támadás” parancs ezt adja a `Damage`-nek |
> **TODO:** A `Combatant` konstruktorának paraméterei és sorrendje, a kezdő `atb` értékek, további mezők (pl. MP, sebesség).
---
## 5. Menük (`menus`)
Karakterenként egy lista, amelynek elemei `(felirat, művelet)` párok:
```python
("Támadás", "phys_att")
```
A `művelet` értéke a `menu_action` függvényben kerül feldolgozásra:
| Művelet | Hatás |
|---|---|
| `"phys_att"` | Célpontválasztó (`"ene"`), majd egy `Action("Támadás", user, target, Damage(user.attack, 0, 0, 0), StatusChange(0, 0, 0), battle_data.anim_default)` kerül a sorba |
| `"magic"` | Megnyitja a képességmenüt (`spells[karakter_index]`) |
| `"items"` | Megnyitja a tárgymenüt (`inventory`) |
| `"unselectable"` | Nem csinál semmit, a kijelölés a menüben marad |
| *bármi más* | Kiírja az `Unknown action` hibát a konzolra, és **bezárja a menüt** (lásd 12. fejezet) |
A menüpontok `x = 19`, `y = 64 - i * 13` pozícióban jelennek meg, ez a menüpanelen belül **legfeljebb 5 sort** jelent (lásd 12. fejezet).
---
## 6. Képességek (`spells`)
Karakterenként egy lista, amelynek elemei:
```python
(név, mp_költség, (akció_név, függvény, célpont_típus))
```
| Elem | Leírás |
|---|---|
| `név` | A képességmenüben megjelenő felirat |
| `mp_költség` | A kódban kicsomagolódik, de a csatamód **nem használja** (nem ellenőrzi, nem vonja le) |
| `akció_név` | A kódban kicsomagolódik, de a csatamód **nem használja** |
| `függvény` | `függvény(user, target) -> Action`. Lásd lent |
| `célpont_típus` | Lásd 8. fejezet |
A kiválasztott célpont után a csatamód ezt hívja:
```python
action_queue.append(függvény(user, targets[0]))
```
Vagyis a függvény két argumentumot kap: **a cselekvő karaktert** és **a kiválasztott egyetlen célpontot**, és egy `Action` objektumot kell visszaadnia.
> **TODO:** Az MP-költség levonása és ellenőrzése. A csatamódban nincs ilyen, tehát vagy az `Action`/animáció végzi, vagy még nincs megvalósítva.
---
## 7. Tárgyak (`inventory`)
Egyetlen, a csata összes karaktere által **közösen** használt lista. Elemei **módosítható listák** (nem tuple-ök, mert a mennyiség csökken):
```python
[név, mennyiség, (akció_név, függvény, célpont_típus)]
```
| Elem | Leírás |
|---|---|
| `név` | A menüben `név xMENNYISÉG` formában jelenik meg (pl. `Gyógyital x3`) |
| `mennyiség` | `int`. A 0 vagy kisebb értékű tárgyak nem jelennek meg |
| `függvény` | Ugyanaz, mint a képességeknél: `függvény(user, target) -> Action` |
| `célpont_típus` | Lásd 8. fejezet |
A mennyiség **a kiválasztáskor** (az akció sorba állításakor) csökken eggyel, nem a végrehajtáskor.
---
## 8. Célpont-típusok
Az `open_target_menu(user, aparty, eparty, target_type, on_pick, back)` a `target_type` alapján állítja össze a választható célpontokat.
> **Dokumentáció és kód eltérése:** A függvény docstringje az `"enemy"`, `"ally"`, `"self"`, `"all_enemies"`, `"all_allies"` értékeket említi, de a tényleges kód más szavakat vizsgál. **A kód a mérvadó.**
| `target_type` | Célpont-lista | Legyőzöttek? | Megjegyzés |
|---|---|---|---|
| `"self"` | A cselekvő maga | Nem releváns | Nincs célpontválasztó, a menü azonnal bezárul |
| `"ene"` | `eparty` (a hősök) | Kimaradnak | A „Támadás” is ezt használja |
| `"ene_rev"` | `eparty` | **Benne vannak** | |
| `"all_rev"` | `aparty` (a saját oldal) | **Benne vannak** | Újraélesztő képességekhez |
| *bármilyen más szöveg* | `aparty` (a saját oldal) | Kimaradnak | Pl. `"ally"` ide esik |
További tudnivalók:
- Az `aparty` a játékos oldala (`party_members`), az `eparty` a hősöké (`enemies`).
- Ha nincs választható célpont, a függvény visszatér, és az előző menü marad nyitva.
- A célpont-visszahívás (`on_pick`) **mindig egyelemű listát** kap, így a csatamód többcélpontos kijelölést **nem támogat**. A célpontválasztó listája a célpont nevét és az aktuális életerejét mutatja, kiegészítve a `Kő` és `Védés` állapotokkal.
- Ha területi hatást akarsz, azt a függvénynek/animációnak kell kezelnie. **TODO:** A függvény csak a `user` és `target` argumentumot kapja, így a többi szereplő eléréséhez a modul szintű listákra (`battle.enemies`, `battle.party_members`) lenne szükség. Ennek a tervezett módja még tisztázandó.
---
## 9. Az MI-függvény (`enemy_turn_func`)
### Aláírás
```python
def hero_ai(e, enemies, party_members, ai_data):
...
return Action(...) # vagy None
```
| Argumentum | Tartalom |
|---|---|
| `e` | A cselekvésre kész hős (`Combatant`) |
| `enemies` | A hősök (saját oldal) listája |
| `party_members` | A játékos karaktereinek listája (az ellenfél, az MI szemszögéből) |
| `ai_data` | A `init_ai_data` csatánkénti másolata. Az MI ebben tárolhat állapotot (pl. számlálót) |
**Visszatérési érték:**
- **`Action`** (vagy bármi igaz értékű): az akció a sor végére kerül, és a hős `atb` értéke `math.inf` lesz.
- **`None` / hamis érték:** a hős nem csinál semmit **ebben a képkockában**, és az MI-függvény **a következő képkockában újra meghívódik**, amíg nem ad vissza akciót. Ez lehetővé teszi, hogy az MI egy feltételre várjon.
### Mikor hívódik?
A függvény minden képkockában (`update`) meghívódik azokra a hősökre, akik:
1. nincsenek legyőzve és nincsenek kővé változva (`not ko and not stone`), és
2. az `atb` értékük `<= 0`.
Minden képkockában minden hős `atb`-ja eggyel csökken (**képkockafüggő**, nem `dt`-alapú. Lásd 12. fejezet).
### Példa
```python
def hero_ai_1(e, enemies, party_members, ai_data):
targets = [p for p in party_members if not p.ko and not p.stone]
if not targets:
return None
ai_data["turn"] += 1
target = random.choice(targets)
return Action(
"Támadás", e, target,
Damage(e.attack, 0, 0, 0), StatusChange(0, 0, 0),
battle_data.anim_default,
)
```
---
## 10. Az `Action` és az akcióanimáció
### Az `Action` felépítése
A kódban látható hívás alapján:
```python
Action(név, user, target, damage, status_change, animation)
```
| Argumentum | Leírás |
|---|---|
| `név` | Az akció neve (a képernyő alján megjelenő panelhez) |
| `user` | A cselekvő `Combatant` |
| `target` | A célpont `Combatant` |
| `damage` | `Damage(a, b, c, d)`. Az első érték a támadásnál a `user.attack` |
| `status_change` | `StatusChange(a, b, c)`. A „Támadás” csupa nullát ad |
| `animation` | Az animációs függvény (lásd lent) |
A csatamód közvetlenül **csak az `animation` mezőt** olvassa. A többi attribútum nevét (pl. `action.user`, `action.target`) az animációknak kell ismerniük.
> **TODO:** Az `Action`, `Damage`, `StatusChange` pontos attribútumnevei és mezőinek jelentése (a `Damage` és `StatusChange` további paraméterei). Ezek a `battle.py`-ban vannak.
> **TODO:** A `battle_data.anim_default` működése.
### Az animációs függvény aláírása
```python
def animation(action, action_text, timer, dt, damage_callback) -> bool:
...
```
| Argumentum | Tartalom |
|---|---|
| `action` | Az éppen futó `Action` objektum |
| `action_text` | A `TextRenderer(122, 204, None, True)` példány, az akciópanel szövege |
| `timer` | Az animáció kezdete óta eltelt idő másodpercben. Az **első híváskor pontosan `0`** |
| `dt` | Az előző képkocka óta eltelt idő |
| `damage_callback` | `damage_callback(amount, target)`. Lebegő szám létrehozása (lásd lent) |
**Visszatérési érték:**
- **`True`**: az animáció véget ért. A csatamód törli az aktuális akciót, és elrejti az akciópanelt.
- **`False`**: az animáció még fut. A `timer` a `dt`-vel nő, és a következő képkockában újra hívódik.
Az akciópanel (`action_panel`) a teljes animáció ideje alatt látható, de az `action_text` szövegét **az animációnak kell beállítania** (pl. `action_text.reset(...)`, `type_all()`), ahogy a menü szövegét is a csatamód teszi a menüben. Az `action_text.render()` minden képkockában lefut, még ha a panel rejtett is, ezért az animáció végén érdemes **kiüríteni a szöveget** (`action_text.reset("")`) **[Feltételezés]**.
### `damage_callback(amount, target)`
| Paraméter | Jelentés |
|---|---|
| `amount` | **Negatív** szám = sebzés (**fehér** számként, előjel nélkül jelenik meg). **Nem negatív** szám = gyógyulás (**zöld** szám) |
| `target` | A célpont `Combatant`. A szám a sprite pozíciója fölött jelenik meg (a `sprite_pos(target)` alapján) |
A callback **csak a lebegő számot** hozza létre. A HP tényleges módosítása és a `ko` beállítása **nem itt** történik, ezeket az animációnak/akciónak kell elvégeznie **[Feltételezés]**.
A lebegő szám 0,5 másodpercig él, és képkockánként `(0, 0.4)` értékkel mozog felfelé.
### Példa (szemléltető jellegű)
```python
def anim_example(action, action_text, timer, dt, damage_callback):
# Első képkocka: szöveg kiírása
if timer == 0:
action_text.reset(action.name) # [Feltételezés] az attribútum neve
action_text.type_all()
# Egyszeri találat 0,6 mp-nél
if timer >= 0.6 and not getattr(action, "_hit", False):
action._hit = True
action.target.hp -= 5 # [Feltételezés] a HP-módosítás az animáció dolga
damage_callback(-5, action.target)
# Vége 1,2 mp-nél
if timer >= 1.2:
action_text.reset("")
return True
return False
```
---
## 11. Egy akció életciklusa
A `update(game, dt)` minden képkockában a következő sorrendben fut:
1. **Hősök időzítője:** minden él(ő), nem kővé vált hősnél `atb -= 1`. Ha `atb <= 0`, meghívódik az `enemy_turn_func`. Ha akciót ad vissza, az a sorba kerül, és `atb = math.inf`.
2. **Játékos karaktereinek időzítője:** ugyanígy `atb -= 1`. Ha `atb <= 0`, a karakter a `menu_queue`-ba kerül, és `atb = math.inf`.
3. **Következő akció indítása:** ha az `action_queue` nem üres és nincs futó akció, az első elem lesz az aktuális (`cur_action`), az akciópanel megjelenik, `action_timer = 0`.
4. **Futó akció léptetése:** `cur_action.animation(...)` meghívása. `True` esetén vége az akciónak, különben `action_timer += dt`.
5. **Menü megnyitása:** ha van várakozó karakter és nincs nyitott menü, megnyílik a következő karakter menüje. A szöveg: „*név* menüje”.
6. **Menüpontok és lebegő számok frissítése.**
7. **Győzelem ellenőrzése:** ha az összes hős `ko` vagy `stone`, a zene megáll, és a játék átvált az `"overworld"` állapotra.
8. **Vereség ellenőrzése:** ha a játékos összes karaktere `ko` vagy `stone`, a zene megáll, és a játék átvált a `"gameover"` állapotra.
A győzelem-ellenőrzés **a vereségé előtt** fut, ezért ha mindkét feltétel egyszerre teljesül, a játékos nyer.
---
## 12. Elrendezés és sprite-ok
### Pozíciók
| Szereplő | Pozíció (képernyőpont) |
|---|---|
| Hős (`i`. index) | `(185, 175 - i * 20)` |
| Játékos karaktere (`i`. index) | `(40 + (i % 2) * 30, 165 - i * 25)` |
A sprite-ok **középpontja** ezen a ponton van. A pozíciók a lista sorrendjét követik. Az alsó menüpanel `230 × 100` méretű, ezért a lista végén lévő szereplők a panel mögé/fölé kerülhetnek (hősöknél kb. a 4. index után, a játékos karaktereinél kb. a 3. index után).
### Három sprite az állapotokhoz
Az `enemy_sprites[i]` és `party_sprites[i]` **pontosan 3 azonosítót** tartalmaz, ebben a sorrendben:
| Index | Mikor látszik |
|---|---|
| `[0]` | Normál állapot |
| `[1]` | Ha `hp <= maxhp * 0.1` (sebzett) |
| `[2]` | Ha `ko` igaz (a KO sprite elsőbbséget élvez) |
Ha a szereplő `stone` állapotban van (és nem `ko`), akkor az éppen kiválasztott sprite **szürkeárnyalatos** változata látszik. A szürke változatot a csatamód létrehozza és gyorsítótárazza.
Az azonosítók az `assets.sprites` szótár kulcsai. Az asset lehet közvetlenül textúra, vagy lehet `.texture` attribútuma.
---
## 13. Ismert korlátok és buktatók
Ezeket a csatamód forrásából olvastam ki. Érdemes őket a csatatervezés közben szem előtt tartani.
1. **Üres képesség- vagy tárgymenü beragadást okoz.** Az `open_spell_menu` és az `open_item_menu` először kiüríti a `menu` listát, és csak utána ellenőrzi, hogy van-e elem. Ha nincs (pl. egy karakternek nincs képessége, vagy elfogytak a tárgyak), a menü üres marad, a `menu_open` pedig nem nullázódik, így a csata ettől a karaktertől kezdve **megakadhat**. Ezért:
- ne szerepeljen `"magic"` a menüben, ha a karakternek nincs képessége,
- ne szerepeljen `"items"`, ha nincs tárgy a csatában, vagy gondoskodni kell róla, hogy a tárgyak ne fogyjanak el.
2. **Ismeretlen menüművelet elveszíti a kört.** A `menu_action` az ismeretlen értéknél bezárja a menüt (`close_menu()`), ezért a karakter `atb` értéke `math.inf` marad, és **nem fog újra cselekedni**, hacsak más nem állítja vissza.
3. **Az `atb` visszaállítása nem a csatamódban történik.** A csatamód az akciók sorba állításakor `math.inf`-re állítja az `atb`-t, de újra nem tölti fel. Ezért az akcióknak/animációknak kell a `user.atb` értékét pozitív számra visszaállítani a végrehajtás után **[Feltételezés, ellenőrizendő a `battle.py`-ban]**. Ha ez elmarad, a karakter többé nem cselekszik.
4. **Az `atb` képkockafüggő.** Az `atb -= 1` minden `update` híváskor lefut, nem a `dt`-től függ. A csata sebessége így a frissítési gyakoriságtól függ.
5. **Legfeljebb 5 menüsor fér el.** A menüpontok `y = 64 - i * 13` pozícióban vannak. A 6. elem már a képernyőn kívülre esik. Ez a képesség-, tárgy- és célpontlistára egyaránt vonatkozik.
6. **Nincs ellenőrzés az akció végrehajtásakor.** A sorba állított akció akkor is lefut, ha közben a cselekvő vagy a célpont legyőzötté vált. Az animációnak/akciónak kell kezelnie ezt az esetet **[Feltételezés]**.
7. **Többcélpontos akció nincs.** A célpontválasztás mindig egyetlen célpontot ad át.
8. **Az MP nem érvényesül** a csatamódban (lásd 6. fejezet).
---
## 14. Nyitott kérdések (TODO)
- A `Combatant`, `Action`, `Damage`, `StatusChange` osztályok teljes definíciója (`battle.py`).
- A `battle_data.anim_default` és az egyéb beépített animációk működése.
- Az `atb` újratöltésének helye és értékei.
- A HP-módosítás, a `ko`, `stone`, `def_up` állapotok beállításának helye.
- Az MP-rendszer megléte és működése.
- A `TextRenderer` és `MenuItem` pontos API-ja (a csatamódban használt hívások: `reset`, `type_all`, `move`, `render`, `select`, `update`).
- A `battle_id` és a csataszobrok összerendelése.