Compare commits

...

24 Commits

Author SHA1 Message Date
markojozsef
b77b110eb9 Nem kell már a bullshit.py 2026-10-07 13:10:49 +02:00
markojozsef
d8ed2afa60 🤦 2026-10-07 13:08:39 +02:00
markojozsef
68a8e44388 még egy screenshot 2026-10-07 13:06:29 +02:00
markojozsef
2a29f318b4 csata menük, csata nyerés, game over képernyő 2026-10-07 13:02:35 +02:00
markojozsef
7ac1a8259f vissza függvény menuItemben 2026-10-07 12:14:49 +02:00
markojozsef
75fc03a88d csata formátum dokumentáció 2026-10-07 12:13:48 +02:00
markojozsef
4de57827b0 AI használat annotálása 2026-10-07 12:01:27 +02:00
markojozsef
b30a7c291b MenuItem és TextRenderer dokumentáció 2026-10-07 11:56:14 +02:00
markojozsef
eb57b16544 Csatakezdés hang 2026-10-05 17:56:16 +02:00
markojozsef
c9b7c0c001 Akció animációk 2026-10-05 17:49:18 +02:00
markojozsef
6a91d3e6db Csata transition 2026-10-04 16:43:56 +02:00
markojozsef
c85c382f34 Menü bugfix és fizikai támadás 2026-10-03 17:05:53 +02:00
markojozsef
3558b7db7a Csata menü próbálkozás 2026-09-30 13:13:16 +02:00
markojozsef
2d1bae444c Még adatcentrikusabb csatarendszer + pár screenshot 2026-09-30 12:17:16 +02:00
markojozsef
5c7db0e77e Csatarendszer adatcentrikusítása 2026-09-30 11:56:08 +02:00
markojozsef
7ba9004a23 Kezdetleges csatarendszer 2026-09-29 16:45:12 +02:00
markojozsef
57cd65b6c5 Hang 2026-09-27 12:30:59 +02:00
markojozsef
372c55caac Scrollozás és normális középigazítás 2026-09-26 16:13:05 +02:00
markojozsef
fcc37089f0 Egy kis mozaik bugfix 2026-09-26 15:35:12 +02:00
markojozsef
7a62dc6ae0 Mozaik effekt 2026-09-25 10:31:07 +02:00
markojozsef
8e060ce43f Overworld rajzolás és "Main Character" kontroller 2026-09-23 12:58:15 +02:00
markojozsef
d97b5b9fb7 bugfixek és chatgpt teszt csatamenet 2026-09-16 18:52:00 +02:00
markojozsef
7ea8df8002 Menü rendszer 2026-09-16 13:03:56 +02:00
markojozsef
a59a205b8f Rebrand és képernyőképek 2026-09-16 12:45:36 +02:00
33 changed files with 1713 additions and 113 deletions

View File

@@ -1,5 +0,0 @@
# TODO
- menü rendszer
- overworld
- csata
- minden más

22
doc/AIHasznalat.md Normal file
View File

@@ -0,0 +1,22 @@
# AI használat a kódban
## Jelző
Teljesen AI-generált szakaszokat egy `# !!! CHATGPT !!!` komment jelzi
## AI generálta komponensek:
### main.py
* Game
### renderers.py
* TextureRenderer
* NineSlicesPanel
### battle_data.py
* csata függvények
### gamestates/battle.py
* menük
## Dokumentáció
Ezen a fájlon kívül minden dokumentáció AI generálta.

444
doc/CsataFormatum.md Normal file
View File

@@ -0,0 +1,444 @@
# Csata objektum
## Szerkezet
```python
{
"enemies": [...],
"party_members": [...],
"enemy_turn_func": ...,
"mus_id": "...",
"init_ai_data": {...},
"menus": [...],
"spells": [...],
"inventory": [...]
}
```
---
## enemies
Az ellenséges egységek listája.
### Típus
```python
list[Combatant]
```
### Példa
```python
"enemies": [
Combatant("Six-Seven kid", 120, 30, 80, 20, 0, 50, False, False),
Combatant("Ice Sprite", 60, 15, 150, 10, 0, 200, False, True),
]
```
Minden elem egy `Combatant` objektum.
---
## party_members
A játékos által irányított karakterek listája.
### Típus
```python
list[Combatant]
```
### Példa
```python
"party_members": [
Combatant("Locke", 80, 45, 130, 15, 10, 10, False, False),
Combatant("Terra", 100, 35, 100, 30, 30, 30, False, False),
]
```
A lista sorrendje határozza meg:
- a karakter pozícióját a csatában,
- a menük hozzárendelését,
- a varázslatok hozzárendelését.
---
## enemy_turn_func
Az ellenséges köröket kezelő függvény.
### Típus
```python
callable
```
### Példa
```python
"enemy_turn_func": enemy_turn_test
```
A függvényt a rendszer az ellenfelek körének feldolgozására használja.
---
## mus_id
A csata alatt lejátszandó zene azonosítója.
### Típus
```python
str
```
### Példa
```python
"mus_id": "mus_fnafsux"
```
---
## init_ai_data
Az MI számára átadott inicializáló adatok.
### Típus
```python
dict
```
---
### moves
Az MI által használható képességek gyűjteménye.
#### Típus
```python
dict[str, tuple]
```
#### Formátum
```python
"azonosító": (
név,
függvény,
célpont_szükséges
)
```
#### Paraméterek
- `név` (`str`)
A képesség megjelenő neve.
- `függvény` (`callable`)
A végrehajtandó függvény.
- `célpont_szükséges`
- `True` → ellenséges célpont szükséges
- `False` → szövetséges célpont szükséges
- `None` → nincs célpont
#### Példa
```python
"attack": (
"Physical attack",
move_attack,
True
)
```
---
### Teljes példa
```python
"moves": {
"attack": ("Physical attack", move_attack, True),
"fire": ("Fira", move_fire, True),
"ice": ("Blizzara", move_ice, True),
"cure": ("Cure", move_cure, False),
"defend": ("Defend", move_defend, None)
}
```
---
## menus
Karakterenként meghatározza a harci főmenüt.
### Típus
```python
list[list]
```
A külső lista a karaktereket reprezentálja.
A sorrendnek meg kell egyeznie a `party_members` listával.
---
### Egy karakter menüje
```python
[
["Attack", "phys_att"],
["Magic", "magic"],
["Item", "items"]
]
```
---
### Menüpont formátuma
```python
[
megjelenő_szöveg,
parancs
]
```
#### Paraméterek
- `megjelenő_szöveg` (`str`)
- `parancs` (`str`)
---
### Speciális parancsok
#### Fizikai támadás
```python
["Attack", "phys_att"]
```
#### Mágia
```python
["Magic", "magic"]
```
#### Tárgyak
```python
["Item", "items"]
```
#### Nem választható hely
```python
["", "unselectable"]
```
Ez egy üres, nem kiválasztható menüpont.
---
## spells
Karakterenként meghatározza az elérhető varázslatokat.
### Típus
```python
list[list]
```
A lista sorrendje megegyezik a `party_members` sorrendjével.
---
### Varázslat formátuma
```python
[
név,
mp_költség,
(
név,
függvény,
célpont_szükséges
)
]
```
#### Paraméterek
- `név` (`str`)
A varázslat megjelenő neve.
- `mp_költség` (`int`)
A varázslat MP költsége.
- `akció` (`tuple`)
A végrehajtandó harci művelet.
---
### Példa
```python
[
"Fira",
15,
("Fira", move_fire, True)
]
```
---
### Teljes példa
```python
"spells": [
[],
[
["Fira", 0, ("Fira", move_fire, True)],
["Blizzara", 0, ("Blizzara", move_ice, True)]
],
[
["Fira", 0, ("Fira", move_fire, True)],
["Blizzara", 0, ("Blizzara", move_ice, True)]
],
[]
]
```
Ebben az esetben:
- Locke nem rendelkezik varázslatokkal,
- Terra használhatja a **Fira** és **Blizzara** varázslatokat,
- Celes használhatja a **Fira** és **Blizzara** varázslatokat,
- Cyan nem rendelkezik varázslatokkal.
---
## inventory
A csatában használható tárgyak listája.
### Típus
```python
list
```
---
### Tárgy formátuma
```python
[
név,
mennyiség,
(
név,
függvény,
célpont_szükséges
)
]
```
#### Paraméterek
- `név` (`str`)
A tárgy megjelenő neve.
- `mennyiség` (`int`)
A rendelkezésre álló darabszám.
- `akció` (`tuple`)
A tárgy használatakor meghívandó művelet.
---
### Példa
```python
[
"Phoenix Down",
67,
(
"Phoenix Down",
move_phoenix,
False
)
]
```
---
### Teljes példa
```python
"inventory": [
[
"Phoenix Down",
67,
(
"Phoenix Down",
move_phoenix,
False
)
]
]
```
---
## Indexkapcsolatok
A következő listák indexei összetartoznak:
```python
party_members[i]
menus[i]
spells[i]
```
Példa:
```python
party_members[1] # Terra
menus[1] # Terra menüje
spells[1] # Terra varázslatai
```
Ez biztosítja, hogy minden karakter saját menüvel és saját varázslistával rendelkezzen.
---
## Összefoglaló
```python
{
"enemies": list[Combatant],
"party_members": list[Combatant],
"enemy_turn_func": callable,
"mus_id": str,
"init_ai_data": {
"moves": dict[str, tuple]
},
"menus": list[list],
"spells": list[list],
"inventory": list
}
```
Egy csata teljes egészében leírható egyetlen szótárral, amely tartalmazza az összes résztvevőt, a harci lehetőségeket, az MI által használható képességeket, valamint a játékos számára elérhető menüket, varázslatokat és tárgyakat.

225
doc/MenuItem.md Normal file
View File

@@ -0,0 +1,225 @@
# MenuItem
## Áttekintés
A `MenuItem` osztály egy játék menürendszerének egyetlen választható elemét reprezentálja. Az osztály kezeli:
- a menüpont szövegének megjelenítését,
- a kijelölést,
- a menüpontok közötti navigációt,
- a kiválasztáskor meghívandó függvényt (akciót),
- valamint a kurzor kirajzolását.
A menüpontok egy irányított hálózatot alkotnak, ahol minden elem ismeri a szomszédait (`up`, `down`, `left`, `right`).
---
## Konstruktor
```python
MenuItem(
label,
x,
y,
left,
right,
up,
down,
action,
data=None,
back_func=None
)
```
### Paraméterek
- `label` (`str`)
A menüpont megjelenő szövege.
- `x` (`int`)
A menüpont vízszintes pozíciója.
- `y` (`int`)
A menüpont függőleges pozíciója.
- `left` (`MenuItem | None`)
A bal oldali szomszédos menüpont.
- `right` (`MenuItem | None`)
A jobb oldali szomszédos menüpont.
- `up` (`MenuItem | None`)
A felső szomszédos menüpont.
- `down` (`MenuItem | None`)
Az alsó szomszédos menüpont.
- `action` (`callable`)
A menüpont kiválasztásakor meghívandó függvény.
- `data` (`any`, opcionális)
A kiválasztáskor az akciónak átadott adat.
- `back_func` (`callable`)
A menüpont kiválasztásakor meghívandó függvény.
---
## Attribútumok
### Navigáció
```python
self.left
self.right
self.up
self.down
```
A szomszédos menüpontokra mutató referenciák.
Ha egy adott irányban nincs további menüpont, akkor az attribútum értéke:
```python
None
```
---
### Akció
```python
self.action
```
A kiválasztás során meghívandó függvény.
Példa:
```python
def start_game(data):
print("Játék indul!")
```
---
### Vissza
```python
self.back
```
A visszalépés során meghívandó függvény.
Példa:
```python
def vissza(data):
open_menu()
```
---
### Adat
```python
self.data
```
A menüelemhez kapcsolódó opcionális adat, amelyet az akció- és visszafüggvény paraméterként kap meg.
Példa:
```python
MenuItem(
"Nehéz",
100, 100,
None, None, None, None,
set_difficulty,
"hard"
)
```
Kiválasztáskor:
```python
set_difficulty("hard")
```
fut le.
---
### Pozíció
```python
self.x
self.y
```
A menüpont képernyőn elfoglalt pozíciója.
---
### Szövegmegjelenítő
```python
self.text
```
Egy `TextRenderer` objektum, amely a felirat megjelenítéséért felel.
---
### Kijelölt állapot
```python
self.selected
```
Logikai érték:
- `True`: a menüpont jelenleg ki van jelölve;
- `False`: a menüpont nincs kijelölve.
---
## Metódusok
### `select()`
```python
select()
```
Kijelöli a menüpontot.
#### Működés
```python
self.selected = True
```
#### Példa
```python
start_button.select()
```
---
### `update()`
```python
update()
```
A menüpont állapotának frissítése.
Ha az elem nincs kijelölve, a függvény azonnal visszatér:
```python
if not self.selected:
return
```

218
doc/TextRenderer.md Normal file
View File

@@ -0,0 +1,218 @@
# TextRenderer
A `TextRenderer` osztály egy egyszerű, karakterenként gépelődő (typewriter effektusú) szövegmegjelenítő. A szöveget sprite-okként tárolja és rendereli, támogatja a sortörést meghatározott szélesség esetén, valamint speciális karakterek kezelését egyedi `display()` függvényeken keresztül.
---
## Konstruktor
```python
TextRenderer(x, y, width=None)
```
Új szövegmegjelenítőt hoz létre.
### Paraméterek
- `x` (`int`) - A szöveg kezdő X koordinátája.
- `y` (`int`) - A szöveg kezdő Y koordinátája.
- `width` (`int | None`) - Opcionális maximális szélesség pixelekben. Túllépés esetén automatikus sortörés történik.
### Attribútumok
- `sprites` - A renderelendő karaktersprite-ok listája.
- `base_x` - Kezdő X koordináta.
- `base_y` - Kezdő Y koordináta.
- `width` - Maximális szövegszélesség.
- `text` - A megjelenítendő szöveg.
- `textptr` - Az aktuálisan feldolgozott karakter indexe.
- `x` - Aktuális rajzolási pozíció X tengelyen.
- `y` - Aktuális rajzolási pozíció Y tengelyen.
---
## `reset(text)`
```python
reset(text)
```
Visszaállítja a renderelőt alapállapotba, és új szöveget tölt be.
### Paraméterek
- `text` (`str`) - A megjelenítendő szöveg.
### Működés
- Törli az összes korábbi sprite-ot.
- Visszaállítja a rajzolási pozíciót a kezdőkoordinátákra.
- Nullázza a karaktermutatót (`textptr`).
### Példa
```python
renderer.reset("Helló világ!")
```
---
## `amend(text)`
```python
amend(text)
```
Hozzáfűz további szöveget a már meglévő szöveghez.
### Paraméterek
- `text` (`str`) - A hozzáfűzendő szöveg.
### Példa
```python
renderer.reset("Helló")
renderer.amend(" világ!")
```
Eredmény:
```text
Helló világ!
```
---
## `type_tick()`
```python
type_tick()
```
Egyetlen karakter feldolgozását végzi el. A függvény elsősorban gépelési animációk megvalósítására használható.
### Visszatérési érték
```python
bool
```
- `True` - a teljes szöveg feldolgozása befejeződött.
- `False` - még vannak feldolgozatlan karakterek.
### Működés
1. Lekéri az aktuális karakterhez tartozó definíciót a `texttable` szótárból.
2. Ismeretlen karakter esetén a `'?'` definíciót használja.
3. Látható karakter esetén:
- kivágja a megfelelő textúrát a font atlaszból,
- létrehozza a sprite adatait,
- hozzáadja azokat a `sprites` listához.
4. Ha a megadott szélesség elfogyott:
- új sorba lép,
- az Y koordinátát 13 pixellel csökkenti.
5. Nem látható karakter esetén meghívja a karakter:
```python
display(renderer)
```
metódusát.
### Példa
```python
while not renderer.type_tick():
pass
```
---
## `type_all()`
```python
type_all()
```
A teljes szöveget azonnal feldolgozza.
### Működés
A `type_tick()` függvényt hívja ismételten addig, amíg minden karakter feldolgozásra nem kerül.
### Példa
```python
renderer.reset("Szia!")
renderer.type_all()
```
A teljes szöveg egyetlen frame alatt elkészül.
---
## `render()`
```python
render()
```
Kirajzolja az összes létrehozott karaktersprite-ot.
### Működés
A `sprites` listán végighaladva minden karakterhez meghívja:
```python
arcade.draw_texture_rect()
```
függvényt pixelpontos (`pixelated=True`) rendereléssel.
### Példa
```python
def on_draw(self):
arcade.start_render()
renderer.render()
```
---
# Tipikus használat
## Gépelési effekt
```python
renderer = TextRenderer(10, 100, width=200)
renderer.reset(
"Ez egy hosszabb szöveg, amely több sorba törhet."
)
renderer.type_tick()
renderer.render()
```
## Azonnali megjelenítés
```python
renderer = TextRenderer(10, 100)
renderer.reset("Helló világ!")
renderer.type_all()
renderer.render()
```
---
# Megjegyzések
- Az osztály nem hoz létre külön `Sprite` objektumot minden karakterhez, hanem csak a szükséges textúrát és pozíciót tárolja.
- Ismeretlen karakter esetén automatikusan a `'?'` karakter kerül megjelenítésre.
- A sortörés kizárólag a megadott `width` túllépésekor történik.
- Speciális vezérlőkarakterek létrehozhatók `visible=False` karakterdefiníciókkal.
- Ezek a karakterek tetszőleges működést valósíthatnak meg a `display(renderer)` metóduson keresztül.

View File

@@ -1,16 +0,0 @@
#define RESOLUTION (1. / iResolution.xy)
#define MAX_MOSAIC 16.
#define SPEED 48.
#define FADEOUT_TIME 1.30
#define FADOUT_SPEED 3.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
float mosaic = floor(((iTime * SPEED) < MAX_MOSAIC) ? iTime * SPEED : (iTime * SPEED) - MAX_MOSAIC) + 1.;
uv = floor(uv / RESOLUTION / mosaic) * RESOLUTION * mosaic;
// Output to screen
fragColor = texture(iChannel0,uv) * max(0., min(1., (FADEOUT_TIME * FADOUT_SPEED) - (iTime * FADOUT_SPEED)));
}

View File

@@ -1,2 +1,2 @@
python -m venv venv
./venv/bin/pip install arcade
./venv/bin/pip install arcade pygame

View File

@@ -1,2 +1,2 @@
python -m venv venv
.\venv\Scripts\pip install arcade
.\venv\Scripts\pip install arcade pygame

BIN
res/mus/fnafsux.wav Normal file

Binary file not shown.

BIN
res/sfx/encounter.wav Normal file

Binary file not shown.

BIN
res/sprite/balthasar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,7 +1,17 @@
import arcade
import pygame
pygame.mixer.init()
sprites = {
"cursor" : arcade.load_texture("res/sprite/handcursor.png"),
"font" : arcade.load_spritesheet("res/sprite/font.png"),
"panel" : arcade.load_spritesheet("res/sprite/panel.png"),
"balthasar": arcade.load_spritesheet("res/sprite/balthasar.png"),
"overworld": arcade.load_spritesheet("res/sprite/overworld_sheet.png"),
}
sounds = {
"mus_fnafsux": pygame.mixer.Sound("res/mus/fnafsux.wav"),
"sfx_encounter": pygame.mixer.Sound("res/sfx/encounter.wav")
}

View File

@@ -3,11 +3,12 @@ import math
K_NORMAL = 100
K_DEFUP = 50
MIN_DMG = 1
BASE_DELAY = 30000
class Combatant:
def __init__(self, name, maxhp, attack, speed, def_phys, def_fire, def_ice, abs_fire, abs_ice):
self.name = name
self.max_atb = 1000/speed
self.max_atb = BASE_DELAY//speed
self.atb = self.max_atb
self.maxhp = maxhp
self.hp = maxhp
@@ -38,12 +39,13 @@ class StatusChange:
# where = reference to the target, who = reference to the inflictor
class Action:
def __init__(self, name, who, where, damage, status):
def __init__(self, name, who, where, damage, status, animation):
self.name = name
self.who = who
self.where = where
self.damage = damage
self.status = status
self.animation = animation
def __str__(self):
return (
@@ -62,7 +64,7 @@ class Action:
self.who.atb = self.who.max_atb
if target.ko:
if target.ko and self.status.ko != 2:
print(f" -> {target.name} is KO'd, move cancelled!")
return
@@ -113,66 +115,3 @@ class Action:
target.stone = False
target.def_up = False
print(f" -> {target.name} got hurt and collapsed!")
enemies = [
Combatant("Six-Seven kid", 100, 100, 100, 100, 100, 100, False, False)
]
party_members = [
Combatant("Terra", 100, 100, 100, 100, 100, 100, False, False),
Combatant("Locke", 100, 100, 100, 100, 100, 100, False, False),
Combatant("Celes", 100, 100, 100, 100, 100, 100, False, False),
Combatant("Cyan", 100, 100, 100, 100, 100, 100, False, False)
]
tick_counter = 0
action_queue = []
while True:
print(f"Tick #{tick_counter}!")
for e in enemies:
if e.ko or e.stone:
continue
e.atb -= 1
if e.atb == 0:
action_queue.append(Action("Physical Attack", e, party_members[0], Damage(25,0,0,0), StatusChange(0, 0, 0)))
for p in party_members:
if p.ko or p.stone:
continue
p.atb -= 1
if p.atb == 0:
print(f"{p.name} is ready to attack with {p.hp} health!")
should_loop = True
while should_loop:
print(f"Action(attack / heal / defend): ", end="")
act = input()
if act == "attack":
action_queue.append(Action("Physical Attack", p, enemies[0], Damage(25,0,0,0), StatusChange(0, 0, 0)))
should_loop = False
elif act == "heal":
action_queue.append(Action("Cure", p, p, Damage(0,0,0,42), StatusChange(0, 0, 0)))
should_loop = False
elif act == "defend":
action_queue.append(Action("Defend", p, p, Damage(0,0,0,0), StatusChange(0, 0, 1)))
should_loop = False
else:
print("Idk what you mean I told you attack and heal and defend are the only valid options bruh")
if len(action_queue) != 0:
action = action_queue.pop(0)
print(f"Executing: {action}")
action.execute()
if all(e.ko for e in enemies):
print("\nVICTORY! all enemies were defeated.")
break
if all(p.ko for p in party_members):
print("\nGAME OVER! the party was defeated.")
break
tick_counter += 1

154
src/battle_data.py Normal file
View File

@@ -0,0 +1,154 @@
import random
from battle import Combatant, Action, Damage, StatusChange
def anim_default(action, text, timer, dt):
if timer == 0:
text.reset(action.name)
text.type_all()
if timer >= 1:
text.reset("")
action.execute()
return True
return False
# !!! CHATGPT !!!
def move_attack(user, target):
return Action("Attack", user, target, Damage(user.attack, 0, 0, 0), StatusChange(0, 0, 0), anim_default)
def move_fire(user, target):
return Action("Fira", user, target, Damage(0, 45, 0, 0), StatusChange(0, 0, 0), anim_default)
def move_ice(user, target):
return Action("Blizzara", user, target, Damage(0, 0, 45, 0), StatusChange(0, 0, 0), anim_default)
def move_cure(user, target):
return Action("Cure", user, target, Damage(0, 0, 0, 42), StatusChange(0, 0, 0), anim_default)
def move_defend(user, target):
return Action("Defend", user, target, Damage(0, 0, 0, 0), StatusChange(0, 0, 1), anim_default)
def move_break(user, target):
return Action("Break", user, target, Damage(0, 0, 0, 0), StatusChange(0, 1, 0), anim_default)
def move_death(user, target):
return Action("Death", user, target, Damage(0, 0, 0, 0), StatusChange(1, 0, 0), anim_default)
def move_phoenix(user, target):
return Action("Phoenix Down", user, target, Damage(0, 0, 0, 30), StatusChange(2, 0, 0), anim_default)
def move_softenup(user, target):
return Action("Esuna", user, target, Damage(0, 0, 0, 0), StatusChange(2, 0, 0), anim_default)
def enemy_turn_test(e,ep, ap,d):
living = [p for p in ap if not p.ko]
if not living:
return None
# weighted move pool -- enemies get the same toolkit as the party
offensive = ["attack", "fire", "ice", "death"]
support = ["break"]
self_only = ["defend"]
# if an ally is KO'd, occasionally try to revive/cure instead of attacking
ko_allies = [a for a in ep if a.ko and a is not e]
hurt_allies = [a for a in ep if not a.ko and a.hp < a.maxhp]
roll = random.random()
if ko_allies and roll < 0.15:
act = "phoenix"
target = random.choice(ko_allies)
elif hurt_allies and roll < 0.30:
act = "cure"
target = random.choice(hurt_allies)
elif not e.def_up and roll < 0.40:
act = "defend"
target = e
elif roll < 0.55:
act = "break"
target = random.choice(living)
elif roll < 0.65:
act = "death"
target = random.choice(living)
else:
act = random.choice(["attack", "fire", "ice"])
target = random.choice(living)
_, builder, _ = d["moves"][act]
print(f" ({e.name} chooses {act} -> {target.name})")
return builder(e, target)
# !!! CHATGPT VÉGE !!!
battles = [
{ "enemies": [
Combatant("Six-Seven kid", 120, 30, 80, 20, 0, 50, False, False),
Combatant("Ice Sprite", 60, 15, 150, 10, 0, 200, False, True ),
Combatant("Magma Brute", 200, 40, 40, 60, 999, -50, True, False),
],
"party_members": [
Combatant("Locke", 80, 45, 130, 15, 10, 10, False, False),
Combatant("Terra", 100, 35, 100, 30, 30, 30, False, False),
Combatant("Celes", 110, 25, 90, 50, 40, 40, False, False),
Combatant("Cyan", 130, 38, 70, 40, 20, 20, False, False),
],
"enemy_turn_func": enemy_turn_test,
"mus_id": "mus_fnafsux",
"init_ai_data": {
"moves": {
"attack": ("Physical attack", move_attack, True),
"fire": ("Fira", move_fire, True),
"ice": ("Blizzara", move_ice, True),
"cure": ("Cure", move_cure, False),
"defend": ("Defend", move_defend, None),
"break": ("Break", move_break, True),
"death": ("Death", move_death, True),
"phoenix": ("Phoenix Down", move_phoenix, False),
"unstone": ("Esuna", move_softenup,False),
}
},
"menus": [
[ # Locke
["Attack", "phys_att"],
["", "unselectable"],
["", "unselectable"],
["Item", "items"]
],
[ # Terra
["Attack", "phys_att"],
["Magic", "magic"],
["", "unselectable"],
["Item", "items"]
],
[ # Celes
["Attack", "phys_att"],
["Magic", "magic"],
["", "unselectable"],
["Item", "items"]
],
[ # Cyan
["Attack", "phys_att"],
["", "unselectable"],
["", "unselectable"],
["Item", "items"]
]
],
"spells": [
[], # Locke
[ # Terra
["Fira", 0, ("Fira", move_fire, True)],
["Blizzara", 0, ("Blizzara", move_ice, True)],
],
[ # Celes
["Fira", 0, ("Fira", move_fire, True)],
["Blizzara", 0, ("Blizzara", move_ice, True)],
],
[] # Cyan
],
"inventory": [
["Phoenix Down",67, ("Phoenix Down", move_phoenix, False)]
]
}
]

View File

@@ -12,6 +12,8 @@ controls = [
Control(arcade.key.S, "Le"),
Control(arcade.key.A, "Bal"),
Control(arcade.key.D, "Jobb"),
Control(arcade.key.ENTER, "Választ"),
Control(arcade.key.LSHIFT, "Vissza"),
]
def key_press(key):
@@ -31,6 +33,11 @@ def update():
c.pressed = False
c.held = True
def depress(cid):
if controls[cid].pressed:
controls[cid].pressed = False
controls[cid].held = True
def is_control_pressed(cid):
return controls[cid].pressed

View File

@@ -1,19 +1,25 @@
import gamestates.none
import gamestates.title
import gamestates.overworld
import gamestates.battle
import gamestates.gameover
gamestates = {
"none": gamestates.none,
"title": gamestates.title
"title": gamestates.title,
"overworld": gamestates.overworld,
"battle": gamestates.battle,
"gameover": gamestates.gameover,
}
gamestate = "none"
def switch_gamestate(state):
def switch_gamestate(game, state):
global gamestate
gamestate = state
gamestates[gamestate].switch()
gamestates[gamestate].switch(game)
def update(dt):
gamestates[gamestate].update(dt)
def update(game, dt):
gamestates[gamestate].update(game, dt)
def draw(game):
gamestates[gamestate].draw(game)

275
src/gamestates/battle.py Normal file
View File

@@ -0,0 +1,275 @@
import math
import copy
import random
import arcade
import gamestate
from battle import Combatant, Action, Damage, StatusChange
from menu import MenuItem
from text import TextRenderer
import assets
import battle_data
enemies = []
party_members = []
enemy_turn = None
battle_music = None
enemy_ai_data = {}
party_menus = []
spells = []
inventory = []
action_queue = []
cur_action = None
action_timer = 0
party_text = TextRenderer(3, 206, 210)
ene_text = TextRenderer(123, 206, 210)
menu_text = TextRenderer(3, 76, 210)
action_text = TextRenderer(110, 215, 210)
menu_open = 0
menu_queue = []
menu = []
battle_id = 0
def switch(game):
global enemies, party_members, battle_music, enemy_turn, enemy_ai_data, party_menus, spells, inventory
print("asdfasdfasdf")
enemies = copy.deepcopy(battle_data.battles[battle_id]["enemies"])
party_members = copy.deepcopy(battle_data.battles[battle_id]["party_members"])
enemy_turn = copy.deepcopy(battle_data.battles[battle_id]["enemy_turn_func"])
enemy_ai_data = copy.deepcopy(battle_data.battles[battle_id]["init_ai_data"])
party_menus = copy.deepcopy(battle_data.battles[battle_id]["menus"])
spells = copy.deepcopy(battle_data.battles[battle_id]["spells"])
inventory = copy.deepcopy(battle_data.battles[battle_id]["inventory"])
battle_music = assets.sounds[battle_data.battles[battle_id]["mus_id"]].play(loops=True)
def open_menu(whose):
party_idx = party_members.index(whose)
for i, m in enumerate(party_menus[party_idx]):
menu.append(MenuItem(m[0], 19, 64-(i*13), None, None, None, None, menu_action, (whose, party_members, enemies, m[1])))
for i, m in enumerate(menu):
m.up = menu[(i - 1) % len(menu)]
m.down = menu[(i + 1) % len(menu)]
menu[0].select()
def close_menu():
global menu, menu_open
menu = []
menu_open = 0
menu_text.reset("")
# !!! CHATGPT !!!
def open_spell_menu(user, aparty, eparty):
global menu
party_idx = party_members.index(user)
spell_list = spells[party_idx]
menu = []
for i, spell in enumerate(spell_list):
menu.append(
MenuItem(
spell[0],
19,
64 - (i * 13),
None,
None,
None,
None,
spell_action,
(user, spell, aparty, eparty)
)
)
if len(menu) == 0:
return
for i, m in enumerate(menu):
m.up = menu[(i - 1) % len(menu)]
m.down = menu[(i + 1) % len(menu)]
menu[0].select()
def spell_action(data):
user, spell, aparty, eparty = data
name, mp_cost, action = spell
action_name, func, target_type = action
action_queue.append(func(user, [p for p in eparty if not p.ko][0]))
close_menu()
def open_item_menu(user, aparty, eparty):
global menu
menu = []
for i, item in enumerate(inventory):
if item[1] <= 0:
continue
menu.append(
MenuItem(
f"{item[0]} x{item[1]}",
19,
64 - (i * 13),
None,
None,
None,
None,
item_action,
(user, item, aparty, eparty)
)
)
if len(menu) == 0:
return
for i, m in enumerate(menu):
m.up = menu[(i - 1) % len(menu)]
m.down = menu[(i + 1) % len(menu)]
menu[0].select()
def item_action(data):
user, item, aparty, eparty = data
name, amount, action = item
action_name, func, target_type = action
action_queue.append(
func(user, user)
)
item[1] -= 1
close_menu()
# !!! CHATGPT VÉGE !!!
def menu_action(data):
user, aparty, eparty, action = data
match action:
case "unselectable":
return
case "phys_att":
action_queue.append(
Action(
"Physical Attack",
user,
[p for p in eparty if not p.ko][0],
Damage(166, 0, 0, 0),
StatusChange(0, 0, 0),
battle_data.anim_default
)
)
close_menu()
case "magic":
open_spell_menu(user, aparty, eparty)
case "items":
open_item_menu(user, aparty, eparty)
case _:
print(f"Unknown action: {action}")
close_menu()
def update(game, dt):
global menu_open, cur_action, action_timer
for e in enemies:
if e.ko or e.stone:
continue
e.atb -= 1
if e.atb <= 0:
act = enemy_turn(e, enemies, party_members, enemy_ai_data)
if act:
action_queue.append(act)
e.atb = math.inf
for p in party_members:
if p.ko or p.stone:
continue
p.atb -= 1
if p.atb <= 0:
print(f"{p.name} is ready to act!")
menu_queue.append(p)
p.atb = math.inf
if len(action_queue) != 0 and cur_action == None:
action = action_queue.pop(0)
print(f"Executing: {action}")
cur_action = action
action_timer = 0
if cur_action != None:
if cur_action.animation(cur_action, action_text, action_timer, dt):
cur_action = None
else:
action_timer += dt
# Szövegek Frissítése
partytext = ""
enetext = ""
for m in party_members:
partytext += f"=={m.name}==\nHP: {m.hp}\n"
for e in enemies:
enetext += f"=={e.name}==\nHP: {e.hp}\n"
if len(menu_queue) != 0 and menu_open == 0:
whose = menu_queue.pop(0)
menu_open = whose
menu_text.reset(f"{whose.name}'s Menu")
menu_text.type_all()
open_menu(whose)
for m in menu:
m.update()
if len(enemies) != 0 and all(e.ko for e in enemies):
print("\nVICTORY! all enemies were defeated.")
battle_music.stop()
gamestate.switch_gamestate(game,"overworld")
return
if len(party_members) != 0 and all(a.ko for a in party_members):
print("\nGAME OVER! the party was defeated.")
battle_music.stop()
gamestate.switch_gamestate(game,"gameover")
return
party_text.reset(partytext)
ene_text.reset(enetext)
party_text.type_all()
ene_text.type_all()
def draw(game):
for m in menu:
m.render()
party_text.render()
ene_text.render()
menu_text.render()
action_text.render()

View File

@@ -0,0 +1,38 @@
import menu
import text
import renderers
import gamestate
text = text.TextRenderer(90, 180, 210)
panel = renderers.NineSlicesPanel(84,100,64,100)
m = []
def retry(g):
gamestate.switch_gamestate(g, "title")
def switch(game):
global m
m = [
menu.MenuItem("Ujrapróba", 98, 150, None, None, None, None, retry, game),
menu.MenuItem("Kilépés", 98, 130, None, None, None, None, quit)
]
m[0].down = m[1]
m[1].up = m[0]
m[0].select()
text.reset("Játék vége!")
text.type_all()
def update(game, dt):
for i in m:
i.update()
def draw(game):
panel.render()
text.render()
for i in m:
i.render()

View File

@@ -1,5 +1,5 @@
import gamestate
# Azért létezik ez a "none" gamestate hogy meghívja az elsö gamestate init() funkcióját
def update(dt):
gamestate.switch_gamestate("title")
def update(game, dt):
gamestate.switch_gamestate(game, "overworld")

View File

@@ -0,0 +1,80 @@
import assets
import arcade
import controls
import gamestate
import math
from objects import BalthasarRenderer, OverworldMap, DIR_UP, DIR_LEFT, DIR_DOWN, DIR_RIGHT
WALK_SPEED = 48
balthasar = BalthasarRenderer()
overworld = OverworldMap()
btrans_active = False
btrans_timer = 0
btrans_sound = None
BTRANS_PULSE_DUR = 0.25
BTRANS_DURATION = 0.75
def start_battle(battle_id):
global btrans_active, btrans_timer, btrans_sound
gamestate.gamestates["battle"].battle_id = battle_id
btrans_sound = assets.sounds["sfx_encounter"].play()
btrans_active = True
btrans_timer = 0
def switch(game):
in_battle_transition = False
def update(game, dt):
global btrans_active, btrans_timer
if btrans_active:
btrans_timer += dt
if btrans_timer >= BTRANS_DURATION:
game.mos_enabled = False
btrans_active = False
btrans_sound.stop()
gamestate.switch_gamestate(game,"battle")
game.mos_enabled = True
cycle_progress = (btrans_timer % BTRANS_PULSE_DUR) / BTRANS_PULSE_DUR
game.mosaic = max(1, math.floor(cycle_progress * 16))
return
balthasar.update()
if controls.is_control_pressed(4): # DEBUG: battle
start_battle(0)
if controls.is_control_pressed(0): # Fel
balthasar.set_direction(DIR_UP)
elif controls.is_control_pressed(1): # Le
balthasar.set_direction(DIR_DOWN)
elif controls.is_control_pressed(2): # Bal
balthasar.set_direction(DIR_LEFT)
elif controls.is_control_pressed(3): # Jobb
balthasar.set_direction(DIR_RIGHT)
walking = False
if controls.is_control_held(0): # Fel
overworld.scroll_y -= WALK_SPEED * dt
walking = True
elif controls.is_control_held(1): # Le
overworld.scroll_y += WALK_SPEED * dt
walking = True
if controls.is_control_held(2): # Bal
overworld.scroll_x += WALK_SPEED * dt
walking = True
elif controls.is_control_held(3): # Jobb
overworld.scroll_x -= WALK_SPEED * dt
walking = True
balthasar.set_walking(walking)
def draw(game):
overworld.render(game)
balthasar.render(game)

View File

@@ -1,21 +1,42 @@
import arcade
import assets
import controls
import menu
import text
import renderers
text = text.TextRenderer(6, 186, 210)
panel = renderers.NineSlicesPanel(0,100,230,100)
def switch():
pass
m = []
def update(dt):
def mos_1x(g):
g.mosaic = 1
g.mos_enabled = False
def mos_2x(g):
g.mosaic = 8
g.mos_enabled = True
def switch(game):
global m
m = [
menu.MenuItem("1", 100, 90, None, None, None, None, mos_1x, game),
menu.MenuItem("2", 100, 70 , None, None, None, None, mos_2x, game)
]
m[0].down = m[1]
m[1].up = m[0]
m[0].select()
def update(game, dt):
controltext = ""
for c in controls.controls:
controltext += f"{c.label}: {c.pressed}-{c.held}\n"
for i in m:
i.update()
text.reset(controltext)
text.type_all()
@@ -24,6 +45,8 @@ def draw(game):
arcade.LBWH(0, 0, game.screen_width, game.screen_height),
arcade.color.RED,
)
for i in m:
i.render()
panel.render()
text.render()

View File

@@ -8,17 +8,20 @@ import text
SCREEN_WIDTH = 256
SCREEN_HEIGHT = 224
# !!! CHATGPT !!!
class Game(arcade.Window):
def __init__(self):
super().__init__(
SCREEN_WIDTH,
SCREEN_HEIGHT,
"Rick Roll To The Moon",
"Defeat the Party",
resizable=True,
)
self.screen_width = SCREEN_WIDTH
self.screen_height = SCREEN_HEIGHT
self.mosaic = 1
self.mos_enabled = False
self.game_texture = self.ctx.texture(
(SCREEN_WIDTH, SCREEN_HEIGHT),
@@ -33,7 +36,7 @@ class Game(arcade.Window):
self.renderer = renderers.TextureRenderer(self.ctx)
def on_update(self, dt):
gamestate.update(dt)
gamestate.update(game, dt)
controls.update()
def on_key_press(self, symbol, modifiers):
@@ -73,6 +76,8 @@ class Game(arcade.Window):
y,
scaled_width,
scaled_height,
self.mosaic,
self.mos_enabled
)
game = Game()

63
src/menu.py Normal file
View File

@@ -0,0 +1,63 @@
import arcade
import assets
import text
import controls
# Választható menüelem több menüelem közt, ha nincs valamelyik irányba másik menüelem, az irányra None értéket adunk
class MenuItem:
def __init__(self, label, x, y, left, right, up, down, action, data=None, back_func=None):
self.left = left
self.right = right
self.up = up
self.down = down
self.action = action
self.back = back_func
self.data = data
self.x = x
self.y = y
self.text = text.TextRenderer(x, y)
self.selected = False
self.text.reset(label)
self.text.type_all()
def select(self):
self.selected = True
def update(self):
if not self.selected:
return
if controls.is_control_pressed(4) and self.action: # Választ
self.action(self.data)
return
if controls.is_control_pressed(5) and self.back: # Vissza
self.back(self.data)
return
if controls.is_control_pressed(0) and self.up: # Fel
controls.depress(0)
self.selected = False
self.up.select()
elif controls.is_control_pressed(1) and self.down: # Le
controls.depress(1)
self.selected = False
self.down.select()
elif controls.is_control_pressed(2) and self.left: # Bal
controls.depress(2)
self.selected = False
self.left.select()
elif controls.is_control_pressed(3) and self.right: # Jobb
controls.depress(3)
self.selected = False
self.right.select()
def render(self):
self.text.render()
if self.selected:
arcade.draw_texture_rect(
assets.sprites["cursor"],
arcade.LBWH(self.x - 16, self.y - 6, 16, 16),
pixelated=True
)

100
src/objects.py Normal file
View File

@@ -0,0 +1,100 @@
import assets
import arcade
# Main character(balthasar) renderer
DIR_DOWN = 0
DIR_UP = 1
DIR_RIGHT = 2
DIR_LEFT = 3
B_SPRITE_WIDHT = 16
B_SPRITE_HEIGHT = 24
B_SPRITE_WALKING = 3
class BalthasarRenderer:
def __init__(self):
self.direction = DIR_UP
self.walking = False
self.anim_timer = 0
def set_direction(self, direction):
self.direction = direction
def set_walking(self, walking):
self.walking = walking
if walking == False:
self.anim_timer = 0
def update(self):
self.anim_timer += 1
if self.anim_timer > 30:
self.anim_timer = 0
def render(self, game):
sprite_h = self.direction if self.direction < 3 else 2
sprite_v = 1 if (self.walking and self.anim_timer <= 15) else 0
flip_h = self.direction == DIR_LEFT
sprite = assets.sprites["balthasar"].get_texture(arcade.LBWH(B_SPRITE_WIDHT*sprite_h, B_SPRITE_HEIGHT*sprite_v, B_SPRITE_WIDHT, B_SPRITE_HEIGHT))
arcade.draw_texture_rect(
sprite.flip_horizontally() if flip_h else sprite,
arcade.LBWH((game.screen_width / 2) - (B_SPRITE_WIDHT / 2), (game.screen_height / 2) - (B_SPRITE_HEIGHT / 2), B_SPRITE_WIDHT, B_SPRITE_HEIGHT),
pixelated=True
)
# Overworld Map
OVERWORLD_LB_X = 0
OVERWORLD_LB_Y = 0
OVERWORLD_TILE_WIDTH = 16
OVERWORLD_TILE_HEIGHT = 16
OVERWORLD_WIDTH = 4
OVERWORLD_HEIGHT = 4
OVERWORLD_MAP = [
[5,3,3,6],
[1,0,0,2],
[1,0,0,2],
[7,4,4,8],
]
class OverworldMap:
def __init__(self):
self.sprites = []
self.scroll_x = 0
self.scroll_y = 0
x = 0
y = len(OVERWORLD_MAP)
for row in OVERWORLD_MAP:
for tile in row:
self.sprites.append([
assets.sprites["overworld"].get_texture(
arcade.LBWH((tile % 4) * OVERWORLD_TILE_WIDTH, (tile // 4) * OVERWORLD_TILE_HEIGHT,
OVERWORLD_TILE_WIDTH, OVERWORLD_TILE_HEIGHT)
),
arcade.LBWH(
(x * OVERWORLD_TILE_WIDTH),
(y * OVERWORLD_TILE_HEIGHT),
OVERWORLD_TILE_WIDTH,
OVERWORLD_TILE_HEIGHT
)
])
x += 1
y -= 1
x = 0
def render(self, game):
for s in self.sprites:
arcade.draw_texture_rect(
s[0],
s[1].move(
self.scroll_x + (game.screen_width / 2) - ((OVERWORLD_WIDTH * OVERWORLD_TILE_WIDTH) / 2),
self.scroll_y + (game.screen_height / 2) - ((OVERWORLD_HEIGHT * OVERWORLD_TILE_HEIGHT) / 2)
),
pixelated=True
)

View File

@@ -34,24 +34,36 @@ class TextureRenderer:
""",
fragment_shader="""
#version 330
#define INV_RESOLUTION_X (1. / bounds.z)
#define INV_RESOLUTION_Y (1. / bounds.w)
uniform vec4 bounds;
uniform sampler2D tex;
uniform float mosaic;
uniform bool mos_enabled;
in vec2 uv;
out vec4 color;
void main() {
color = texture(tex, uv);
vec2 new_uv = vec2(
(floor((uv.x / INV_RESOLUTION_X) / mosaic) * INV_RESOLUTION_X) * mosaic,
(floor((uv.y / INV_RESOLUTION_Y) / mosaic) * INV_RESOLUTION_Y) * mosaic
);
color = texture(tex, mos_enabled ? new_uv : uv);
}
""",
)
self.quad = arcade.gl.geometry.quad_2d_fs()
def render(self, texture, x, y, width, height):
def render(self, texture, x, y, width, height, mosaic, mos_enabled):
self.program["screen_size"] = (self.ctx.screen.width,
self.ctx.screen.height)
self.program["bounds"] = (x, y, width, height)
self.program["mosaic"] = mosaic
self.program["mos_enabled"] = mos_enabled
texture.use(0)
self.program["tex"] = 0