Metadata-Version: 2.4
Name: Tiavina.engine.gl
Version: 0.3.0
Summary: Moteur de jeu 2D sur PyOpenGL (GLFW + Pillow)
License-Expression: GPL-3.0-only
Project-URL: Homepage, https://github.com/andyravelo4-code/
Keywords: game,engine,opengl,2d,pixel
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Games/Entertainment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyOpenGL>=3.1
Requires-Dist: glfw>=2.5
Requires-Dist: Pillow>=9.0
Requires-Dist: miniaudio>=1.71
Dynamic: license-file

# Tiavina.engine.gl

Moteur de jeu 2D pixel art, construit sur **PyOpenGL + GLFW + Pillow**.  
Fonctionne avec un **écran virtuel** (résolution logique) mis à l'échelle entière vers la fenêtre d'affichage.

## Installation

```bash
pip install Tiavina.engine.gl
```

```python
from Engine import engine as e

e.init(200, 200, "Mon Jeu", fps=60, display_scale=4)

def update():
    if e.btn(e.KEY_ESCAPE):
        e.quit()

def draw():
    e.cls((30, 30, 40))
    e.rect(10, 10, 20, 20, (255, 0, 0))

e.run(update, draw)
```

---

## Table des matières

- [Installation](#installation)

- [Architecture](#architecture)
- [Initialisation](#initialisation)
- [Boucle principale](#boucle-principale)
- [Graphismes — `Graphics`](#graphismes--graphics)
  - [Primitives de dessin](#primitives-de-dessin)
  - [Couleurs avec alpha (RGBA)](#couleurs-avec-alpha-rgba)
  - [Blit d'image](#blit-dimage)
  - [Texte](#texte)
  - [Caméra](#caméra)
  - [Clipping](#clipping)
- [Caméra intelligente — `Camera`](#caméra-intelligente--camera)
- [Entrées — `Input`](#entrées--input)
  - [Clavier](#clavier)
  - [Souris](#souris)
  - [Joystick](#joystick)
- [Ressources — `Resources`](#ressources--resources)
- [Audio — `Audio`](#audio--audio)
- [Police pixel — `default_font`](#police-pixel--default_font)
- [Constantes](#constantes)
- [Fonctions globales](#fonctions-globales)
- [Exemple complet](#exemple-complet)

---

## Architecture

```
Engine/
├── __init__.py        ← réexporte l'API
├── engine.py          ← le moteur (tout-en-un)
└── fonts/
    └── PressStart2P.ttf   ← police pixel embarquée
```

Le moteur expose une API **globale** : on importe `from Engine import engine as e` et on appelle `e.init()`, `e.cls()`, `e.btn()`, etc.

Sous le capot :

- **`App`** — classe principale qui gère la fenêtre GLFW, l'horloge FPS, le `virtual_screen` (PIL Image logique), et la boucle d'événements.
- **`Graphics`** — dessin sur l'écran virtuel avec support de caméra, clipping, et alpha. OpenGL est utilisé pour le blit final sur l'écran physique.
- **`Input`** — gestion clavier et souris via GLFW.
- **`Resources`** — chargement d'images.
- **`Audio`** — **stub** (non implémenté, retourne `None`).

---

## Initialisation

### `e.init(width, height, title="T.engine", fps=30, display_scale=1, pixel_art=True)`

Crée la fenêtre et initialise tous les sous-systèmes.

| Paramètre | Type | Description |
|-----------|------|-------------|
| `width` | `int` | Largeur logique de l'écran virtuel (ex: 200) |
| `height` | `int` | Hauteur logique (ex: 200) |
| `title` | `str` | Titre de la fenêtre |
| `fps` | `int` | Images par seconde cibles |
| `display_scale` | `int` | Facteur d'échelle entier (ex: 5 → fenêtre 1000×1000 pour 200×200) |
| `pixel_art` | `bool` | `True` = arrondit caméra et coordonnées, ligne 1px logique → `display_scale` px physiques (défaut: `True`) |

Quand `pixel_art=True`, la caméra est arrondie aux entiers, les positions de dessin sont arrondies aux entiers, et `glLineWidth(display_scale)` est utilisé pour les lignes de 1px logique. Mettre à `False` pour un rendu haute résolution sans ces contraintes.

L'écran virtuel fait `width × height` pixels. Chaque frame, il est scaled par `display_scale` et affiché dans la fenêtre système.

```python
e.init(200, 200, "Mon Jeu", 60, 5)
```

### `e.run(update, draw)`

Lance la boucle de jeu. `update()` est appelée à chaque frame pour la logique, `draw()` pour le rendu.

```python
def update():
    pass

def draw():
    e.cls((0, 0, 0))

e.run(update, draw)
```

### `e.quit()`

Ferme Pygame et termine le processus.

### `e.width()`, `e.height()`

Retourne la largeur/hauteur logique définies dans `init()`.

### `e.frame_count()`

Retourne le nombre de frames écoulées depuis `init()`.

---

## Graphismes — `Graphics`

Toutes les fonctions de dessin **sont affectées par la caméra** (sauf si `camera()` est réinitialisée).  
Les coordonnées sont en pixels logiques.

### Primitives de dessin

| Fonction | Description |
|----------|-------------|
| `cls(color)` | Remplit tout l'écran virtuel avec une couleur |
| `pset(x, y, color)` | Dessine un pixel |
| `pget(x, y) → Color` | Lit la couleur d'un pixel |
| `line(x1, y1, x2, y2, color)` | Ligne |
| `rect(x, y, w, h, color)` | Rectangle plein |
| `rectb(x, y, w, h, color)` | Rectangle vide (1px de bord) |
| `circ(x, y, r, color)` | Cercle plein |
| `circb(x, y, r, color)` | Cercle vide (1px de bord) |
| `elli(x, y, w, h, color)` | Ellipse pleine |
| `ellib(x, y, w, h, color)` | Ellipse vide (1px de bord) |
| `tri(x1, y1, x2, y2, x3, y3, color)` | Triangle plein |
| `trib(x1, y1, x2, y2, x3, y3, color)` | Triangle vide (1px de bord) |

### Couleurs avec alpha (RGBA)

Toutes les primitives sauf `text()` et `pget()` acceptent des couleurs **RGBA** (4 valeurs).  
L'alpha est géré via un blending additif sur une surface temporaire.

```python
# Rectangle semi-transparent
e.rect(10, 10, 50, 50, (255, 0, 0, 128))
```

### Blit d'image

```python
blt(x, y, img, u, v, w, h, colkey=None, rotate=0)
```

| Paramètre | Type | Description |
|-----------|------|-------------|
| `x, y` | `int` | Position de destination (coin supérieur gauche) |
| `img` | `PIL.Image` ou `_Img` | Image source |
| `u, v` | `int` | Coordonnées source (coin supérieur gauche dans l'image) |
| `w, h` | `int` | Dimensions de la région source |
| `colkey` | `Color` ou `None` | Couleur de transparence (optionnelle) |
| `rotate` | `float` | Rotation en degrés horaire (pivot au centre) |

```python
e.blt(100, 100, img, 0, 0, 16, 16)          # blit simple
e.blt(100, 100, img, 0, 0, 16, 16, rotate=45)  # rotation
```

### Texte

```python
text(x, y, s, color, font=None)
```

| Paramètre | Type | Description |
|-----------|------|-------------|
| `x, y` | `int` | Position |
| `s` | `str` | Texte à afficher |
| `color` | `tuple` | Couleur RGB (pas de support alpha) |
| `font` | `FontWrap` ou `None` | Police personnalisée (None = police pixel par défaut) |

La police par défaut est **PressStart2P.ttf** taille 6px, rendu **sans anti-aliasing** pour un aspect pixel art.

```python
e.text(5, 5, "Score: 42", (255, 255, 255))

# Avec une police différente
big = e.default_font(16)
e.text(5, 20, "Titre", (255, 200, 0), font=big)
```

### Caméra

```python
camera(dx, dy)    # Applique un décalage à tout le dessin suivant
camera()          # Réinitialise le décalage à (0, 0)
```

La caméra est cumulative : tous les appels `camera()` s'ajoutent.  
Pour un système plus avancé, voir la [classe `Camera`](#caméra-intelligente--camera).

```python
e.camera(-player.x + 100, -player.y + 100)   # suit le joueur
```

### Clipping

```python
clip(x, y, w, h)    # Active un rectangle de clipping
clip()              # Désactive le clipping
```

Le clipping est appliqué après la caméra.

---

## Caméra intelligente — `Camera`

```python
cam = e.Camera(target, screen_width, screen_height,
               mouse_influence=0.2, mouse_limit=10)
```

Classe réutilisable avec suivi de cible, offset souris, tremblement et flash.

### Paramètres du constructeur

| Paramètre | Défaut | Description |
|-----------|--------|-------------|
| `target` | — | Objet suivi (doit avoir `.x`, `.y`) |
| `screen_width` | — | Largeur de l'écran logique |
| `screen_height` | — | Hauteur de l'écran logique |
| `mouse_influence` | `0.2` | Sensibilité du regard vers la souris (0.0–1.0) |
| `mouse_limit` | `10` | Décalage maximal dû à la souris (en pixels logiques) |

### Méthodes

| Méthode | Description |
|---------|-------------|
| `update()` | Calcule la nouvelle position de la caméra (target + souris + shake) |
| `apply()` | Applique le décalage au moteur graphique (`e.camera(cam.cam_x, cam.cam_y)`) |
| `shake(duration, intensity)` | Déclenche un tremblement d'écran |
| `flash(color, alpha, duration)` | Déclenche un flash d'écran |

### Attributs

| Attribut | Description |
|----------|-------------|
| `cam_x, cam_y` | Position calculée de la caméra (en pixels logiques) |
| `flash_color` | Couleur du flash actif |
| `flash_alpha` | Opacité courante du flash |
| `shake_intensity` | Intensité du tremblement actuel |

Le flash doit être dessiné manuellement dans `draw()` :

```python
if cam.flash_alpha > 0:
    e.camera()
    e.rect(0, 0, e.width(), e.height(), (*cam.flash_color, cam.flash_alpha))
```

---

## Entrées — `Input`

### Clavier

```python
btn(key)        # → True si la touche est maintenue
btnp(key)       # → True le frame où la touche est pressée (pas de répétition)
btnr(key)       # → True le frame où la touche est relâchée
```

```python
if e.btn(e.KEY_SPACE):
    player.jump()
if e.btnp(e.KEY_E):
    player.interact()
```

### Souris

```python
mouse_x(), mouse_y()     # → Position logique (divisée par display_scale)
mouse_btn(button)        # → True si le bouton est maintenu
mouse_btnp(button)       # → True le frame du clic
mouse_btnr(button)       # → True le frame du relâchement
mouse(visible=True)      # → Affiche ou cache le curseur système
```

Boutons : `MOUSE_BUTTON_LEFT` (1), `MOUSE_BUTTON_MIDDLE` (2), `MOUSE_BUTTON_RIGHT` (3)

```python
if e.mouse_btnp(e.MOUSE_BUTTON_LEFT):
    print(f"Clic à ({e.mouse_x()}, {e.mouse_y()})")
```

### Joystick

Non implémenté dans `Tiavina.engine.gl` (stub).

---

## Ressources — `Resources`

```python
resources.image(bank, path, colkey=None)   # → PIL.Image ou None
resources.sound(bank, path)                 # → DecodedSoundFile ou None
resources.music(bank, path)                 # → None (enregistre le chemin)
resources.load(filename)                    # → lève NotImplementedError
```

### Images

`image()` charge une image dans `resources.images[bank]`.

- `bank` : index numérique (`int`)
- `path` : chemin relatif ou absolu
- `colkey` : couleur de transparence optionnelle (ex: `(255, 0, 255)`)
- Retourne une `PIL.Image` (convertie en RGBA)

```python
e.resources.image(0, "./sprites/player.png")
e.resources.image(1, "./sprites/tiles.png", (255, 0, 255))

# Utilisation
img = e.resources.images[1]
```

### Sons

`sound()` charge et décode un fichier audio (WAV, MP3, FLAC, OGG) via **miniaudio** et le stocke dans `resources.sounds[bank]` sous forme de `DecodedSoundFile` (PCM en mémoire).

```python
e.resources.sound(0, "./sfx/jump.wav")
e.resources.sound(1, "./music/ambient.ogg")
```

### Musique

`music()` enregistre le chemin d'un fichier dans `resources.musics[bank]` pour lecture différée via `audio.playm()`.

```python
e.resources.music(0, "./music/boss.ogg")
```

---

## Audio — `Audio`

Moteur audio basé sur **miniaudio**. 8 canaux (0–7) pour les effets sonores, 1 flux pour la musique.

### Initialisation

L'`Audio` est créé automatiquement par `e.init()`. Les dictionnaires `sounds` et `musics` sont partagés avec `Resources` — les sons chargés via `e.resources.sound()` sont directement accessibles.

### Méthodes

| Méthode | Description |
|---------|-------------|
| `play(ch, s, loop=False)` | Joue le son `s` (clé) sur le canal `ch` (0–7). `loop=True` → boucle infinie |
| `playm(m, loop=False)` | Joue la musique `m` (clé) en streaming. Arrête la musique précédente |
| `stop(ch=None)` | Stoppe le canal `ch`, ou tous les canaux + musique si `ch=None` |
| `play_pos(ch) → bool` | `True` si le canal `ch` est en train de jouer |

### Détails techniques

- Les sons sont **décodés en mémoire** au chargement (`miniaudio.decode_file()`) → lecture instantanée sans latence disque.
- Chaque canal son crée son propre `miniaudio.PlaybackDevice` avec un **générateur** qui yield les trames PCM.
- La musique est lue en **streaming** (`miniaudio.stream_file()`) pour éviter de charger des fichiers longs en mémoire.
- Formats supportés : **WAV, MP3, FLAC, OGG**.
- Si `miniaudio` n'est pas installé, l'audio est désactivé silencieusement (`_HAS_AUDIO = False`, toutes les méthodes sont des no-op).

### Exemples

```python
# Charger des sons
e.resources.sound(0, "./sfx/jump.wav")
e.resources.sound(1, "./sfx/hit.wav")
e.resources.music(0, "./music/theme.ogg")

# Jouer
e.audio.play(0, 0)              # jump sur canal 0
e.audio.play(1, 1)              # hit sur canal 1
e.audio.playm(0)                # musique de fond
e.audio.play(2, 0, loop=True)   # jump en boucle sur canal 2

# Vérifier
if e.audio.play_pos(0):
    print("Canal 0 joue encore")

# Arrêter
e.audio.stop(0)                 # stop canal 0 seulement
e.audio.stop()                  # stop tout (canaux + musique)
```

---

## Police pixel — `default_font`

```python
default_font(size=6, antialias=False) → FontWrap
```

Charge et met en cache la police **PressStart2P.ttf** via Pillow (`ImageFont.truetype`).  
L'anti-aliasing est désactivé par défaut pour un rendu pixel art.

La police est stockée dans un cache global `_pixel_font_cache` : chaque taille n'est chargée qu'une fois.

```python
petite = e.default_font(6)
moyenne = e.default_font(8, antialias=True)   # avec lissage
grande  = e.default_font(16)
```

---

## Constantes

### Touches clavier

`KEY_A`–`KEY_Z`, `KEY_0`–`KEY_9`,
`KEY_SPACE`, `KEY_UP`, `KEY_DOWN`, `KEY_LEFT`, `KEY_RIGHT`,
`KEY_ESCAPE`, `KEY_RETURN`, `KEY_TAB`, `KEY_BACKSPACE`,
`KEY_LSHIFT`, `KEY_RSHIFT`, `KEY_LCTRL`, `KEY_RCTRL`,
`KEY_LALT`, `KEY_RALT`

Toutes sont des constantes GLFW (`glfw.KEY_*`) réexportées par le module.

### Boutons souris

| Constante | Valeur |
|-----------|--------|
| `MOUSE_BUTTON_LEFT` | `1` |
| `MOUSE_BUTTON_MIDDLE` | `2` |
| `MOUSE_BUTTON_RIGHT` | `3` |

---

## Fonctions globales

Le module `engine` expose des fonctions globales qui délèguent aux sous-systèmes internes :

| Fonction | Délègue à |
|----------|-----------|
| `cls(c)` | `graphics.cls(c)` |
| `pset(x,y,c)` | `graphics.pset(x,y,c)` |
| `pget(x,y)` | `graphics.pget(x,y)` |
| `line(x1,y1,x2,y2,c)` | `graphics.line(...)` |
| `rect(x,y,w,h,c)` | `graphics.rect(...)` |
| `rectb(x,y,w,h,c)` | `graphics.rectb(...)` |
| `circ(x,y,r,c)` | `graphics.circ(...)` |
| `circb(x,y,r,c)` | `graphics.circb(...)` |
| `elli(x,y,w,h,c)` | `graphics.elli(...)` |
| `ellib(x,y,w,h,c)` | `graphics.ellib(...)` |
| `tri(x1,y1,x2,y2,x3,y3,c)` | `graphics.tri(...)` |
| `trib(x1,y1,x2,y2,x3,y3,c)` | `graphics.trib(...)` |
| `text(x,y,s,c,font)` | `graphics.text(...)` |
| `blt(x,y,img,u,v,w,h,...)` | `graphics.blt(...)` |
| `bltm(x,y,tm,u,v,w,h,...)` | `graphics.bltm(...)` |
| `clip(x,y,w,h)` | `graphics.clip(...)` |
| `camera(x,y)` | `graphics.camera(...)` |
| `pal(c1,c2)` | `graphics.pal(...)` (stub) |
| `dither(a)` | `graphics.dither(...)` (stub) |
| `mouse_btn(b)` | `input.mouse_btn(b)` |
| `mouse_btnp(b)` | `input.mouse_btnp(b)` |
| `mouse_btnr(b)` | `input.mouse_btnr(b)` |
| `btn(k)` | `input.btn(k)` |
| `btnp(k)` | `input.btnp(k)` |
| `btnr(k)` | `input.btnr(k)` |

---

## Exemple complet

```python
from Engine import engine as e
from random import randint

# ── Initialisation ─────────────────────────────────────
e.init(200, 200, "Aventurier", fps=60, display_scale=5,
       pixel_art=True)

# ── Ressources ─────────────────────────────────────────
e.resources.image(0, "./sprites/player.png")
e.resources.image(1, "./sprites/tiles.png")

# ── Joueur ─────────────────────────────────────────────
class Player:
    def __init__(self):
        self.x = 100
        self.y = 100
        self.w = 8
        self.h = 8
        self.vie = 100

player = Player()

# ── Caméra ─────────────────────────────────────────────
cam = e.Camera(player, e.width(), e.height(),
               mouse_influence=0.3, mouse_limit=12)

# ── Boucle ─────────────────────────────────────────────
def update():
    # Déplacements
    if e.btn(e.KEY_LEFT):  player.x -= 1
    if e.btn(e.KEY_RIGHT): player.x += 1
    if e.btn(e.KEY_UP):    player.y -= 1
    if e.btn(e.KEY_DOWN):  player.y += 1
    if e.btn(e.KEY_ESCAPE): e.quit()

    # Clic pour shake
    if e.mouse_btnp(e.MOUSE_BUTTON_LEFT):
        cam.shake(10, 6)
        cam.flash((255, 255, 200), 60, 4)

    cam.update()
    cam.apply()

def draw():
    e.cls((30, 30, 40))

    # Sol (en coordonnées monde)
    for i in range(-2, 30):
        for j in range(-2, 20):
            e.rect(i * 8, j * 8, 8, 8, (40, 45, 55))

    # Joueur
    e.rect(player.x, player.y, player.w, player.h,
           (100, 200, 255) if player.vie > 0 else (255, 50, 50))

    # Flash overlay (dessiné avant la réinitialisation caméra)
    if cam.flash_alpha > 0:
        e.camera()
        e.rect(0, 0, e.width(), e.height(),
               (*cam.flash_color, cam.flash_alpha))

    # ── HUD (coordonnées écran) ──
    e.camera()  # ← réinitialise la caméra

    e.text(4, 4, f"Vie: {player.vie}", (255, 255, 255))
    e.text(4, 12, f"Pos: {player.x},{player.y}", (200, 200, 200))

    # Curseur
    e.circb(e.mouse_x(), e.mouse_y(), 3, (255, 255, 255, 80))

e.run(update, draw)
```
