diff --git a/docs/engine-design.md b/docs/engine-design.md new file mode 100644 index 0000000..6d2547e --- /dev/null +++ b/docs/engine-design.md @@ -0,0 +1,104 @@ +# TetaRdPG — Engine Design + +> Version : 1.0 — 2026-03-25 +> Décision architecturale : le moteur RPG est un template neutre, l'Odyssée est sa première customisation. + +--- + +## Principe + +``` +Couche 1 — Moteur (template, forkable) + CombatService → attaque / fuir, formules dégâts GDD + Stats → Force, Agilité, Intelligence, Chance, Vitalité + Économie → Or, monnaie premium, endurance + Progression → XP, niveaux, déblocages + Forge / Craft → amélioration, recettes, matériaux + Quêtes → fetch, complete, rewards + Achievements → event-driven, paliers + +Couche 2 — Univers (spécifique à l'Odyssée, pas dans le template) + TurnManager → tour par tour enrichi (sorts, compagnons, patterns IA) + SpellSystem → 3 voies du Dao, arbre de sorts, mana/endurance + CompanionModule → Mira/Vell, IA contextuelle + NarrativeEngine → PNJ évolutifs, dialogues par palier, fragments du Chant + BossPhaseSystem → mécaniques uniques (ex: guérison Hydre ≠ DPS) +``` + +## Règle + +Le moteur (couche 1) ne doit **jamais** dépendre de la couche 2. + +Un fork du moteur = combat basique fonctionnel, zéro référence à l'Odyssée. +L'Odyssée importe le moteur, pas l'inverse. + +## Impact sur le combat rework + +Le `CombatService` actuel n'est pas jeté — il devient le moteur de base. +Le rework **étend** sans casser : + +``` +CombatService (existant) → reste, calcule dégâts/XP/loot + └── TurnManager (nouveau) → orchestre les tours, gère l'ordre + ├── SpellSystem → sorts par voie, coût, effets + ├── CompanionAI → décisions IA Mira/Vell + └── BossPhaseManager → phases spéciales (narratif) +``` + +## La Métamorphose — Evoland pattern (décision 2026-03-25) + +Finir les 3 arcs de l'Acte I (Marais → Égouts → Désert) déclenche **Le Serment des Trois**. +Le jeu lui-même évolue — le joueur vit la métamorphose du têtard. + +``` +ACTE I (niv 1-13) — Le monde simple + Combat : auto (POST /combat/start) + Slots : 🗡️ Arme + 🛡️ Armure (2 slots) + Types : weapon | armor | consumable + Sorts : aucun + Compagnons : aucun + + ⬇️ LE SERMENT DES TROIS ⬇️ + (quête narrative, arc desert complété) + +ACTE II (niv 13+) — Le monde éveillé + Combat : tour par tour (POST /combat/turn/start) + Slots : 🗡️ Main droite + 🛡️ Main gauche + 🪖 Casque + 👕 Armure + 💍 Anneau (5 slots) + OU ⚔️ Arme deux mains (remplace main droite + gauche) + Types : weapon_1h | weapon_2h | shield | helmet | armor | ring | consumable + Sorts : Dao du Courant (3 voies, 15 sorts) + Compagnons : Mira, Vell (quêtes narratives) +``` + +### Règle de coexistence + +- Zones 1-3 gardent le combat simple (grind rapide, ×5/×10 toujours dispo) +- Zones 4+ forcent le combat tour par tour (narratif, sorts, compagnons) +- Les items Acte I restent utilisables — les nouveaux types n'existent qu'en Acte II +- Le joueur garde tout (or, items, stats) — c'est une évolution, pas un reset + +### Impact sur l'Item entity + +``` +Acte I (existant, inchangé) : + ItemType = 'weapon' | 'armor' | 'consumable' + equipped = boolean (1 arme + 1 armure max) + +Acte II (extension) : + ItemType += 'weapon_2h' | 'shield' | 'helmet' | 'ring' + equipped_slot = 'main_hand' | 'off_hand' | 'head' | 'body' | 'ring' | null + Contrainte : weapon_2h → off_hand bloqué +``` + +### Pourquoi c'est cohérent + +Le GDD original prévoit des déblocages par niveau (forge niv 10, boutique avancée niv 15). +L'Acte II est le plus gros déblocage du jeu — le gameplay entier évolue. +Narrativement : le têtard commence à se transformer. Le Dao s'éveille. + +## Conséquence template + +Quand un streamer fork TetaRdPG pour son royaume (Phase 4 — Twitch Kingdom) : +- Il hérite du moteur complet (couche 1) +- Il peut remplacer l'Odyssée par son propre lore (couche 2) +- Ou il joue dans l'univers partagé de l'Odyssée — c'est son choix diff --git a/docs/lore-bible.md b/docs/lore-bible.md new file mode 100644 index 0000000..f59b665 --- /dev/null +++ b/docs/lore-bible.md @@ -0,0 +1,432 @@ +# TetaRdPG — Lore Bible + +> Version : 1.0 — 2026-03-25 +> Source : "L'Odyssée d'un têtard" (histoire originale par Tetardtek) +> Statut : Canon de base — à étoffer par game-designer + storyman après combat rework +> Objectif : Ancrer l'univers, les personnages, les lieux et la mythologie du jeu + +--- + +## Univers + +### Le monde + +Un univers aquatique et amphibien où l'eau est le tissu de la réalité. Les étangs, ruisseaux, marais et torrents forment un réseau vivant relié par le **Courant** — une force mystique qui traverse toute chose. L'eau n'est pas un décor, c'est le medium de la magie, de la mémoire et du destin. + +### Mythologie fondatrice + +Au commencement, l'eau **chantait**. Un chant pur, tissé de toutes les vies passées et futures, maintenait l'harmonie du monde. Ce chant s'est tu. L'équilibre s'est rompu. Les gardiens anciens ont sombré dans l'oubli ou le chaos. + +La prophétie dit que le chant reviendra quand l'eau trouvera son **Élu** — celui qui saura écouter, résonner et harmoniser. + +### Le Dao du Courant + +Philosophie et système de magie central. Le Dao du Courant est l'art de danser avec l'eau plutôt que de la combattre. Trois voies le composent : + +| Voie | Principe | Style | Archétype | +|------|----------|-------|-----------| +| **Écoute** | Percevoir les flux invisibles | Contrôle, stratégie, chant offensif | Tetardtek — le stratège | +| **Résonance** | Amplifier la force par l'harmonie | Tank, protection, contre-attaque | Vell — le protecteur | +| **Harmonie** | Tisser l'équilibre entre les êtres | Support, heal, apaisement | Mira — l'harmoniste | + +> Le joueur n'est pas enfermé dans une voie. Sa voie principale progresse plus vite, mais il peut explorer les trois. La voie se révèle progressivement via les choix narratifs. + +--- + +## Personnages + +### Personnages principaux + +#### Tetardtek — "Celui qui flotte entre les mondes" + +- **Rôle :** Protagoniste / avatar du joueur +- **Personnalité :** Observateur, curieux, déterminé. Il perçoit ce que les autres ignorent. +- **Arc :** De têtard rêveur à porteur du Chant. Apprend l'Écoute — percevoir les courants invisibles, éveiller les mémoires enfouies. +- **Pouvoir :** Perception du Courant, Chant restaurateur, ancrage mémoriel +- **Citation :** *"Était-il destiné à rester, ou son avenir se trouvait-il ailleurs ?"* + +#### Vell — Le rival devenu frère + +- **Rôle :** Rival amical → compagnon de combat +- **Personnalité :** Impétueux, fier, loyal. Confond d'abord force et valeur. +- **Arc :** De la force brute à la maîtrise — apprend que la vraie puissance est dans l'écoute, pas la domination. Son tournant : le Torrent Brisé (ch.11). +- **Pouvoir :** Résonance — onde de choc, bouclier de flux, contre-attaque amplifiée +- **Citation :** *"La vraie force... c'est de ne pas lutter seul."* +- **En jeu :** PNJ au village (arène), compagnon de combat, défis rivalité + +#### Mira — L'harmoniste naturelle + +- **Rôle :** Compagnon support / guide émotionnel +- **Personnalité :** Douce, intuitive, silencieusement puissante. Connectée au Courant sans le savoir. +- **Arc :** De la grâce inconsciente à la maîtrise assumée. Son tournant : le Ruisseau Miroir (ch.9), où elle accepte son chant. +- **Pouvoir :** Harmonie — chant guérisseur, apaisement, dissolution des illusions +- **Citation :** *"Le chant guérit. Mais il faut l'entendre. Et l'accepter."* +- **En jeu :** PNJ au village (place centrale, quêtes narratives, heal), compagnon de combat + +#### Gorn — L'ancien + +- **Rôle :** Mentor, gardien du savoir ancien +- **Personnalité :** Sage, grave, bienveillant. Porte un poids qu'il n'a jamais partagé. +- **Arc :** Guide les héros puis se sacrifie face à l'Hydre (ch.7). Son héritage persiste à travers la Pierre-Mémoire et ses échos. +- **Pouvoir :** Connaissance du Dao, lien avec la Pierre-Mémoire, lumière protectrice +- **Citation :** *"Le courant t'attend. Va. Moi, je dois clore un cycle."* +- **En jeu :** PNJ au village (tableau des quêtes) niv 1-15. Après ch.7 → remplacé par la Pierre-Mémoire (ses échos guident le joueur) + +### Entités mystiques + +#### L'Anguille de l'Oubli + +- **Nature :** Gardienne des souvenirs engloutis — ni alliée, ni ennemie +- **Apparence :** Ombre longue et sinueuse, yeux vides et profonds +- **Rôle narratif :** Oracle et seuil. Apparaît aux moments charnières. Son regard perce les âmes. +- **Citation :** *"Tu crois que l'étang est éternel ? L'instant viendra où tu devras choisir : flotter ou bondir."* +- **En jeu :** Boss/oracle récurrent. Apparaît à chaque palier de niveau clé. Pas un ennemi à tuer — un test à comprendre. + +#### L'Hydre des Profondeurs + +- **Nature :** Ancienne gardienne des eaux, corrompue par la rupture du Chant +- **Apparence :** Immense, sinueuse, écailles sombres et luisantes, éclat rouge au cœur +- **Vérité cachée :** L'Hydre était la chantre des profondeurs — aimée, vénérée. Quand le Chant a été brisé, elle a sombré dans le chaos. Elle n'est pas le mal, elle est la douleur. +- **Arc :** Antagoniste → être à guérir. Le combat final n'est pas une mise à mort mais une restauration du Chant. +- **En jeu :** Boss mid-game (ch.7, destruction de l'étang) + boss final (ch.14, guérison par le Chant) + +#### L'Éclaireuse du Courant (libellule dorée) + +- **Nature :** Messagère du Dao. Apparaît aux âmes prêtes. +- **Apparence :** Libellule dorée, ailes translucides veinées d'argent, projette des symboles lumineux +- **Rôle :** Signal visuel et narratif — marque les moments d'éveil +- **En jeu :** Créature guide, indices visuels in-game, notifications de progression narrative + +#### La Batracienne du Marais + +- **Nature :** Sage ancienne, gardienne des visions +- **Apparence :** Batracienne vêtue de lianes et d'algues, yeux voilés mais brillants de lumière verte +- **Rôle :** Donne les visions du passé, révèle la vérité sur l'Hydre +- **Citation :** *"Tu portes le chant en germe, petit têtard. Mais avant la source, il faut comprendre le courant."* +- **En jeu :** PNJ zone Marais des Murmures — quêtes vision, unlock lore profond + +### Faune de l'étang (PNJ secondaires / ambiance) + +| Créature | Rôle | Trait | +|----------|------|-------| +| Escargots aquatiques | Ambiance, PNJ marchands | Lents, sages, tracent des sillons sur les pierres | +| Notonectes | Éclaireurs, sentinelles | Planent à la surface, fuient au danger | +| Poissons-argent | Ambiance, indicateurs d'état du monde | Nuées brillantes — vifs = étang sain, figés = danger | +| Larves de libellules | Gardiennes des recoins | Patientes, guetteuses | +| Gerris | Patrouilleurs de surface | Insectes géants pour un têtard | +| Crapaud-moine | Sage secondaire | Chante des mantras anciens | + +--- + +## Lieux + +### L'Étang — Zone de départ (niv 1-15) + +Berceau du joueur. Univers miniature harmonieux… jusqu'à la venue de l'Hydre. + +| Sous-zone | Chapitres | Niveaux | Ambiance | +|-----------|-----------|---------|----------| +| Surface de l'Étang | 1-3 | 1-5 | Paisible, tutoriel, nénuphars, roseaux, lucioles | +| Profondeurs de l'Étang | 4-6 | 6-10 | Mystérieux, Pierre-Mémoire, lueurs violettes | +| L'Étang Brisé | 7-8 | 11-15 | Dévasté, roseaux flétris, eau trouble, départ | + +**Événements clés :** +- Apparition de l'Anguille de l'Oubli (ch.1) +- Première tentative / vision de la Grenouille émeraude (ch.2) +- Course Tetardtek vs Vell / éveil du Dao (ch.3-4) +- Pierre-Mémoire / transmission de Gorn (ch.5-6) +- Destruction par l'Hydre / sacrifice de Gorn / Serment des Trois (ch.7-8) + +### Ruisseau Miroir — Zone 4 (niv 16-18) + +Eau claire comme du verre fondu, pierres polies. Montre ce qu'on ne veut pas voir. + +| Élément | Description | +|---------|-------------| +| Mécanique | Les ennemis sont des **reflets** du joueur — doubles sombres avec ses propres stats | +| Test | Affronter ses peurs et ses doutes | +| Résolution | Mira chante, dissipe les illusions — le groupe traverse ensemble | +| PNJ | Aucun — zone de solitude intérieure | + +### Marais des Murmures — Zone 5 (niv 19-21) + +Terres basses, brume, arbres morts. L'eau retient les souvenirs et les murmure. + +| Élément | Description | +|---------|-------------| +| Ambiance | Oppressant, chuchotements, voix dans le courant | +| PNJ clé | La Batracienne — donne visions, révèle la vérité sur l'Hydre | +| Révélation | L'Hydre était gardienne avant d'être chaos — le Chant dévoyé l'a brisée | +| Mécanique | Quêtes vision — le joueur vit des flashbacks du passé | +| Loot | Fragments du Chant (collectibles narratifs) | + +### Torrent Brisé — Zone 6 (niv 22-24) + +Eaux violentes, rochers acérés, grondement permanent. La force brute ne passe pas. + +| Élément | Description | +|---------|-------------| +| Mécanique | Zone haute difficulté — les attaques physiques sont réduites par le courant | +| Test | Vell comprend que la force s'écoute, ne se force pas | +| Résolution | Coopération : Mira lit les remous, Tetardtek fredonne, Vell nage entre eux | +| Boss zone | Le Torrent lui-même — obstacle vivant à traverser en groupe | + +### Source du Courant — Zone 7 (niv 25) + +Vasque de pierre, eau en fils de lumière, harmonie. Lieu légendaire de pouvoir. + +| Élément | Description | +|---------|-------------| +| Événement | Palier max Phase 3 — les trois héros reçoivent la pleine maîtrise du Dao | +| Visions | Chacun a une vision personnelle (Tetardtek → Gorn apaisé, Mira → mille voix, Vell → sa force tranquille) | +| Pouvoir | Filament de lumière violette entre en chacun — unlock arbre de sorts complet | +| Loot | Artefact de la Source — item légendaire lié à la voie du joueur | + +### Courants d'Épreuve — Zone 8 (niv 26-28, endgame Phase 3) + +Trois épreuves qui testent la maîtrise du Dao. + +| Épreuve | Test | Voie principale | +|---------|------|-----------------| +| Vents Croisés | Lac suspendu, vents destructeurs — harmoniser le chaos | Harmonie (Mira) | +| Remous de l'Oubli | Zone stagnante qui avale les souvenirs — ancrer la mémoire | Écoute (Tetardtek) | +| Enclave des Roches | Cascade souterraine bloquée — résonance pour ouvrir | Résonance (Vell) | + +### L'Étang Restauré — Zone 9 (niv 29-30, boss final) + +Retour à l'étang — mais transformé, en attente de guérison. + +| Élément | Description | +|---------|-------------| +| Boss final | L'Hydre des Profondeurs — combat en 3 phases | +| Mécanique unique | Pas de DPS brut. Restaurer le Chant via les 3 voies synchronisées | +| Résolution | L'Hydre est guérie, réintégrée à l'harmonie. L'étang revit. | +| Épilogue | Le Chant résonne à nouveau. "L'odyssée des trois héros commençait vraiment." | + +### Le Delta — Tease Phase 4 (niv 30+) + +> *"Il existe d'autres étangs, d'autres courants. Et tous chantent leur propre mélodie."* + +Le Delta est le réseau des courants — là où les étangs se connectent. C'est le setup narratif pour **Twitch Kingdom** : chaque streamer = un étang, chaque communauté = un courant. + +--- + +## Artefacts & Symboles récurrents + +| Artefact | Signification | Rôle en jeu | +|----------|--------------|-------------| +| **La Pierre-Mémoire** | Pierre ancienne gravée de symboles, contient les échos du passé | Remplace Gorn comme guide après ch.7 — hub de quêtes narratives | +| **Le fragment de coquille** | Mémoire laissée par Gorn, gravée de la route vers la Source | Item de quête — boussole narrative | +| **Les Fragments du Chant** | Morceaux de la mélodie perdue, dispersés dans les zones | Collectibles — rassembler = unlock fin vraie | +| **La lumière violette** | Filament mystique liant les porteurs du Dao | Marqueur visuel récurrent — progression, éveil, lien | +| **Les cercles et spirales** | Symboles projetés par l'Éclaireuse | Langage visuel du Courant — UI motif | + +--- + +## Arc narratif global — Structure + +``` +Acte I — L'Étang (ch.1-8, niv 1-15) + Naissance → Éveil → Rivalité → Dao → Mentor → Catastrophe → Serment → Départ + Thème : "Qui suis-je ?" + +Acte II — Le Voyage (ch.9-13, niv 16-28) + Miroir → Murmures → Torrent → Source → Épreuves + Thème : "Que puis-je devenir ?" + +Acte III — Le Retour (ch.14-15, niv 29-30) + Combat final → Guérison de l'Hydre → Restauration du Chant + Thème : "Quel monde vais-je construire ?" + +Épilogue — L'Onde Infinie (ch.15bis, niv 30+) + Tease Phase 4 — d'autres étangs, d'autres courants + Thème : "L'odyssée ne fait que commencer." +``` + +--- + +## Tons et ambiances par zone + +| Zone | Palette | Son | Émotion | +|------|---------|-----|---------| +| Étang (début) | Bleu-vert, argenté, lucioles | Eau calme, roseaux, nuit | Sérénité, curiosité | +| Étang (profond) | Violet sombre, lueurs dorées | Échos, vibrations basses | Mystère, révélation | +| Étang (brisé) | Gris-noir, rouge | Grondement, silence | Terreur, perte, détermination | +| Ruisseau Miroir | Cristal, reflets parfaits | Silence troublant, distorsion | Doute, confrontation | +| Marais des Murmures | Vert sombre, brume, lueur verte | Chuchotements, brume | Oppression, sagesse cachée | +| Torrent Brisé | Blanc d'écume, gris roche | Rugissement, fracas | Épreuve, dépassement | +| Source du Courant | Or, lumière pure, translucide | Harmonie, chant pur | Transcendance, plénitude | +| Courants d'Épreuve | Variable par épreuve | Variable | Maîtrise, derniers doutes | +| Étang Restauré | Arc-en-ciel aquatique | Chant complet | Victoire, émotion, paix | + +--- + +## Hub Village — PNJ et évolution + +Le village (dashboard) évolue avec l'histoire : + +### Avant ch.7 (niv 1-15) + +| Lieu | PNJ | Fonction | +|------|-----|----------| +| Place centrale | Mira | Quêtes narratives, heal entre combats | +| Arène | Vell | Défis combat, entraînement, rivalité | +| Tableau des quêtes | Gorn | Quêtes principales, lore, conseils | +| Forge | Forgeron | Craft & amélioration (existant) | +| Boutique | Marchand | Shop (existant) | +| Portail des zones | — | Accès biomes | + +### Après ch.7 (niv 15+) + +| Changement | Détail | +|------------|--------| +| Gorn disparaît | Remplacé par la **Pierre-Mémoire** — ses échos guident les quêtes | +| Vell évolue | Dialogues mûris, reconnaît la valeur de l'écoute | +| Mira évolue | Assume son rôle de chanteuse, donne des quêtes de chant | +| Ambiance | L'étang est brisé — le village montre les stigmates | + +### Après ch.14 (niv 30) + +| Changement | Détail | +|------------|--------| +| Étang restauré | Le village retrouve sa beauté, amplifiée | +| Gorn en écho | La Pierre-Mémoire montre parfois son visage, apaisé | +| Portail du Delta | Nouveau point d'accès — tease Phase 4 | + +--- + +## Compagnons de combat + +Mira et Vell accompagnent le joueur dans certaines zones clés : + +| Compagnon | Disponible | Style IA | +|-----------|-----------|----------| +| Mira | Quêtes narratives + zones 4-9 | Heal si HP < 40%, buff sinon, chant si boss | +| Vell | Défis arène + zones 6-9 | Protège si joueur en danger, contre-attaque, onde de choc | + +> Les compagnons ne sont pas permanents — ils rejoignent sur les quêtes narratives clés et les zones de l'odyssée. Le joueur combat seul sur le contenu libre (grind, quêtes secondaires). + +--- + +## La Métamorphose — Le jeu évolue avec le joueur + +> Inspiré d'Evoland : le jeu lui-même change quand l'histoire le justifie. + +L'Acte I est le monde simple. Le têtard découvre, apprend, combat basiquement. +Quand il prête le Serment des Trois (fin de l'Acte I), **le jeu se transforme** : + +| Aspect | Acte I (niv 1-13) | Acte II (niv 13+) | +|--------|-------------------|-------------------| +| Combat | Auto (attaque simple) | Tour par tour (sorts, stratégie) | +| Équipement | 2 slots (arme + armure) | 5 slots (main droite, main gauche/bouclier, casque, armure, anneau) | +| Armes | Une seule catégorie | 1 main vs 2 mains (choix tactique) | +| Magie | Aucune | Dao du Courant (3 voies, 15 sorts) | +| Compagnons | Seul | Mira et Vell rejoignent le combat | +| Grind | ×1/×5/×10 auto | Tour par tour narratif + grind simple en zones 1-3 | + +> Le joueur ne perd rien — il gagne. C'est la métamorphose du têtard. +> Les anciens items restent valides. Les nouveaux slots s'ajoutent, ils ne remplacent pas. +> Le joueur peut toujours retourner grind les zones 1-3 en combat simple pour farmer. + +--- + +## Système de combat — Direction + +> Le combat simple (Acte I) reste le moteur de base. Le tour par tour (Acte II) s'y ajoute. +> Design technique complet : voir `docs/engine-design.md` + +### Vision cible + +``` +Tour du joueur : + [Attaque] [Sorts (voie du Dao)] [Items] [Fuir] + +Tour du compagnon (si présent) : + IA contextuelle selon le rôle (Mira = support, Vell = tank) + +Tour de l'ennemi : + Pattern par monstre (agressif, défensif, chaotique) +``` + +### Sorts par voie (exemples à étoffer par game-designer) + +| Voie | Sorts exemple | Effet | +|------|--------------|-------| +| Écoute | Perception du flux | Révèle faiblesses ennemies (1 tour) | +| Écoute | Chant d'éveil | Dégâts magiques + debuff confusion | +| Écoute | Ancrage mémoriel | Annule le prochain debuff | +| Résonance | Onde de choc | Dégâts AoE physiques | +| Résonance | Bouclier de flux | Réduit dégâts reçus (2 tours) | +| Résonance | Contre-courant | Riposte automatique au prochain coup | +| Harmonie | Chant apaisant | Heal moyen | +| Harmonie | Dissolution | Retire les buffs ennemis | +| Harmonie | Onde de sérénité | Buff défense + regen toute l'équipe | + +### Combat final — mécanique unique + +Le boss final (Hydre) ne se bat pas en DPS. Trois phases : +1. **Contenir** — Vell résonne, absorbe le chaos +2. **Apaiser** — Mira chante, ouvre une brèche dans la douleur +3. **Restaurer** — Tetardtek chante le Chant complet → l'Hydre est guérie + +> Cette mécanique sera le climax du jeu. Elle doit être narrativement et mécaniquement différente de tout le reste. + +--- + +## Fragments du Chant — Collectibles narratifs + +Morceaux de la mélodie perdue, dispersés dans les zones. Les rassembler débloque la capacité de guérir l'Hydre. + +| Fragment | Zone | Comment l'obtenir | +|----------|------|------------------| +| Fragment de l'Éveil | Étang (surface) | Quête ch.1 — écouter l'Anguille | +| Fragment de la Vision | Étang (profondeur) | Quête ch.4 — course + éveil du Dao | +| Fragment de la Mémoire | Étang (profondeur) | Quête ch.6 — Pierre-Mémoire | +| Fragment du Serment | Étang (brisé) | Quête ch.8 — le serment des trois | +| Fragment du Miroir | Ruisseau Miroir | Vaincre son reflet | +| Fragment des Murmures | Marais des Murmures | Vision de la Batracienne | +| Fragment du Torrent | Torrent Brisé | Traverser ensemble | +| Fragment de la Source | Source du Courant | Toucher la vasque | + +> 8 fragments = Chant complet → unlock combat final de guérison + +--- + +## Glossaire du canon + +| Terme | Définition | +|-------|-----------| +| **Le Courant** | Force mystique traversant toute eau — medium de la magie et de la mémoire | +| **Le Dao du Courant** | Art de danser avec l'eau. Trois voies : Écoute, Résonance, Harmonie | +| **Le Chant Perdu** | Mélodie ancienne qui maintenait l'harmonie du monde. Brisé, à restaurer. | +| **La Marée Silencieuse** | Phénomène rare — le temps se fige, les mondes se frôlent | +| **La Pierre-Mémoire** | Pierre ancienne contenant les échos de toutes les vies passées | +| **L'Onde Infinie** | Le réseau des courants reliant tous les étangs — métaphore du Delta (Phase 4) | +| **Flotter ou bondir** | Choix fondamental — rester dans le confort ou embrasser son destin | + +--- + +## Notes pour le game-designer et le storyman + +### Ce qui est posé (canon non négociable) +- Les 4 personnages principaux et leurs arcs +- Le Dao du Courant comme système de magie à 3 voies +- L'Hydre = gardienne corrompue à guérir, pas un ennemi à tuer +- Le Chant Perdu comme fil rouge narratif +- La structure en 3 actes + épilogue + +### Ce qui est à étoffer +- **Dialogues** — chaque PNJ a besoin de lignes de dialogue par palier de niveau +- **Quêtes secondaires** — le squelette narratif est là, il faut du contenu entre les quêtes principales +- **Bestiaire** — les monstres de chaque zone (noms, lore, lien au Courant) +- **Items narratifs** — équipements liés à l'histoire (Bâton de Gorn, Collier de Mira, etc.) +- **Événements de zone** — micro-événements aléatoires liés au lore (apparition de l'Éclaireuse, murmures, marée silencieuse) +- **Le passé de l'Hydre** — développer l'époque dorée avant la rupture du Chant +- **L'histoire entre les chapitres** — ce que le joueur vit pendant le grind, pas juste les moments clés +- **Le Delta (Phase 4)** — architecture narrative pour Twitch Kingdom + +### Principe directeur +> L'histoire est courte mais dense. Chaque chapitre est un noyau narratif. +> Le game-designer ajoute du gameplay entre les noyaux. +> Le storyman ajoute de la chair narrative autour des noyaux. +> Le squelette ne change pas. La chair peut être aussi épaisse qu'on veut. diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 8e8eeb1..8221dd9 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -16,9 +16,13 @@ export const RARITY_LABELS: Record = { }; export const ZONE_INFO: Record = { - marais: { name: 'Les Marais', emoji: '🌿', color: '#3ddc84' }, - egouts: { name: 'Les Égouts', emoji: '🕳️', color: '#5ba4f5' }, - desert: { name: 'Le Désert', emoji: '🏜️', color: '#f4c94e' }, + marais: { name: 'Les Marais', emoji: '🌿', color: '#3ddc84' }, + egouts: { name: 'Les Égouts', emoji: '🕳️', color: '#5ba4f5' }, + desert: { name: 'Le Désert', emoji: '🏜️', color: '#f4c94e' }, + ruisseau_miroir: { name: 'Ruisseau Miroir', emoji: '🪞', color: '#88c8e8' }, + marais_murmures: { name: 'Marais des Murmures', emoji: '🌫️', color: '#6b8a6b' }, + torrent_brise: { name: 'Torrent Brisé', emoji: '🌊', color: '#4a7ab5' }, + source_courant: { name: 'Source du Courant', emoji: '✨', color: '#d4af37' }, }; export const STAT_LABELS: Record = { diff --git a/package.json b/package.json index d7d223d..91f6ef6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "seed": "ts-node -r tsconfig-paths/register src/database/seed.ts", "seed:monsters": "ts-node -r tsconfig-paths/register src/database/monsters-seed.ts", "seed:items": "ts-node -r tsconfig-paths/register src/database/items-seed.ts", + "seed:odyssee": "ts-node -r tsconfig-paths/register src/database/seed-odyssee.ts", "typeorm": "typeorm-ts-node-commonjs -d src/database/data-source.ts", "test": "jest", "test:watch": "jest --watch", diff --git a/src/app.module.ts b/src/app.module.ts index 2fbc8ad..b9405af 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,6 +19,7 @@ import { HallOfFameModule } from './halloffame/halloffame.module'; import { ProfileModule } from './profile/profile.module'; import { QuestModule } from './quest/quest.module'; import { ShopModule } from './shop/shop.module'; +import { NpcModule } from './npc/npc.module'; import { HealthController } from './common/health.controller'; @Module({ @@ -61,6 +62,7 @@ import { HealthController } from './common/health.controller'; ProfileModule, QuestModule, ShopModule, + NpcModule, ], controllers: [HealthController], }) diff --git a/src/common/zone-access.ts b/src/common/zone-access.ts index 77005e6..2977f7c 100644 --- a/src/common/zone-access.ts +++ b/src/common/zone-access.ts @@ -3,10 +3,14 @@ import { PlayerQuestArc } from '../quest/player-quest-arc.entity'; import { QuestArc } from '../quest/quest-arc.entity'; // Zone unlock chain: each zone requires completing the previous zone's arc -// marais → always open -// egouts → requires "Les Marais du Têtard" arc completed -// desert → requires the egouts arc completed -const ZONE_ORDER = ['marais', 'egouts', 'desert']; +// marais → always open (L'Étang — niv 1-5) +// egouts → requires "Les Marais du Têtard" arc (L'Étang profond — niv 6-10) +// desert → requires egouts arc (L'Étang Brisé — niv 11-15) +// ruisseau_miroir → requires desert arc (Ruisseau Miroir — niv 16-18) +// marais_murmures → requires ruisseau_miroir arc (Marais des Murmures — niv 19-21) +// torrent_brise → requires marais_murmures arc (Torrent Brisé — niv 22-24) +// source_courant → requires torrent_brise arc (Source du Courant — niv 25+) +const ZONE_ORDER = ['marais', 'egouts', 'desert', 'ruisseau_miroir', 'marais_murmures', 'torrent_brise', 'source_courant']; export async function getUnlockedZones( characterId: string, diff --git a/src/database/data-source.ts b/src/database/data-source.ts index 00ddddb..bbbcad6 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -24,6 +24,10 @@ import { Quest } from '../quest/quest.entity'; import { QuestArc } from '../quest/quest-arc.entity'; import { PlayerQuest } from '../quest/player-quest.entity'; import { PlayerQuestArc } from '../quest/player-quest-arc.entity'; +import { Spell } from '../combat/turn/spell.entity'; +import { PlayerSpell } from '../combat/turn/player-spell.entity'; +import { PlayerDaoPath } from '../combat/turn/player-dao-path.entity'; +import { Npc } from '../npc/npc.entity'; // DataSource pour le CLI TypeORM (migrations manuelles) export const AppDataSource = new DataSource({ @@ -54,6 +58,10 @@ export const AppDataSource = new DataSource({ QuestArc, PlayerQuest, PlayerQuestArc, + Spell, + PlayerSpell, + PlayerDaoPath, + Npc, ], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, diff --git a/src/database/odyssee-quests-seed.ts b/src/database/odyssee-quests-seed.ts new file mode 100644 index 0000000..f20e1f8 --- /dev/null +++ b/src/database/odyssee-quests-seed.ts @@ -0,0 +1,420 @@ +import { DataSource } from 'typeorm'; +import { QuestArc } from '../quest/quest-arc.entity'; +import { Quest } from '../quest/quest.entity'; + +/** + * Seed Phase 3 — L'Odyssée d'un têtard + * + * Chaque quête raconte une scène de l'histoire. + * Les story_event sont des moments narratifs purs (auto-complete). + * Les combats sont légers et ont du sens (1 boss, pas 10 mobs). + * Les textes (acceptText / completeText) sont les dialogues du PNJ. + */ +export async function seedOdysseeQuests(dataSource: DataSource) { + const arcRepo = dataSource.getRepository(QuestArc); + const questRepo = dataSource.getRepository(Quest); + + const monsters = await dataSource.query('SELECT id, name FROM monsters'); + const m = new Map(monsters.map((r: any) => [r.name, r.id])); + + const materials = await dataSource.query('SELECT id, name FROM materials'); + const mat = new Map(materials.map((r: any) => [r.name, r.id])); + + let questsAdded = 0; + + // ═══════════════════════════════════════════════════ + // ARC 4 — LE RUISSEAU MIROIR (ch.9) + // "L'eau claire comme du verre montre ce qu'on ne veut pas voir." + // ═══════════════════════════════════════════════════ + + let arc4 = await arcRepo.findOne({ where: { name: 'Le Ruisseau Miroir' } }); + if (!arc4) { + arc4 = await arcRepo.save(arcRepo.create({ + name: 'Le Ruisseau Miroir', + description: 'Gorn en parlait : un ruisseau qui montre ce qu\'on ne veut pas voir. Mira, Vell et toi devez l\'affronter ensemble.', + zone: 'ruisseau_miroir', + sortOrder: 4, + minLevel: 15, + })); + } + + const arc4Quests = [ + { + name: 'Le Serment des Trois', + description: 'Après la destruction de l\'étang, Mira et Vell vous rejoignent. Ensemble, vous faites un serment.', + acceptText: 'Mira tend sa nageoire : « Que le courant, même blessé, nous porte ensemble. »\nVell la touche, puis vous. Une onde frémit autour de vous, comme une promesse.\nSous une pierre fissurée, un objet luit : un fragment de coquille laissé par Gorn. Dessus, un mot : « Continue. »', + completeText: 'Le Serment des Trois est scellé. Votre chemin est tracé. L\'Odyssée commence vraiment.', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 400, + rewardGold: 200, + rewardTitle: null as string | null, + arcId: arc4.id, + arcOrder: 1, + zone: null as string | null, + minLevel: 15, + repeatable: false, + }, + { + name: 'L\'Eau qui ne Ment Pas', + description: 'Le Ruisseau Miroir projette des doubles sombres de vous-même. Ils murmurent vos peurs.', + acceptText: 'Mira fronce les sourcils : « Le Ruisseau Miroir. Gorn en parlait. Il montre ce que l\'on ne veut pas voir. »\nVous plongez dans une eau si transparente que le ciel s\'y reflète sans déformation. Puis les reflets changent. Votre double vous fixe, le regard dur : « Tu crois comprendre le courant ? Tu n\'es qu\'un songeur. »', + completeText: 'Le Reflet s\'efface dans un éclat de cristal. Ses derniers mots résonnent encore... mais ils n\'ont plus de pouvoir sur vous.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Reflet Sombre') ?? null, + objectiveCount: 1, + rewardXp: 500, + rewardGold: 200, + rewardTitle: null, + arcId: arc4.id, + arcOrder: 2, + zone: 'ruisseau_miroir', + minLevel: 15, + repeatable: false, + }, + { + name: 'L\'Épreuve de Vell', + description: 'Vell affronte son propre reflet. Il doit accepter que la force brute ne suffit pas.', + acceptText: 'Le double de Vell rit, moqueur : « Tu n\'as rien protégé. Gorn est parti, l\'étang est mort. À quoi sers-tu, si tu ne peux frapper assez fort ? »\nVell hurle, frappe l\'eau. Mais le doute s\'insinue dans ses membres. Il se fige.\nVous devez l\'aider — vaincre les Échos de Doute qui l\'emprisonnent.', + completeText: 'Mira chante une note pure. La torpeur cède. Vell redresse la tête, tremblant mais libre : « Ensemble, nous sommes plus que nos reflets. »', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Écho de Doute') ?? null, + objectiveCount: 2, + rewardXp: 700, + rewardGold: 300, + rewardTitle: null, + arcId: arc4.id, + arcOrder: 3, + zone: 'ruisseau_miroir', + minLevel: 16, + repeatable: false, + }, + { + name: 'Le Chant de Mira', + description: 'Mira fait face à son reflet. Il lui pose la question qu\'elle évite depuis toujours.', + acceptText: 'Le reflet de Mira chante doucement. Une mélodie pure. Puis il cesse, la fixant :\n« Le chant est en toi. Pourquoi le caches-tu ? Peur de briser l\'harmonie ? Ou peur de ce que tu deviendrais ? »\nMira ferme les yeux. Quand elle les rouvre, elle chante. Fort. L\'eau vibre autour d\'elle.', + completeText: 'Le Ruisseau Miroir frémit sous le chant de Mira. Les reflets tremblent, se fissurent, et disparaissent.\nMira sourit, grave : « Le chant guérit. Mais il faut l\'entendre. Et l\'accepter. »', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 600, + rewardGold: 250, + rewardTitle: null, + arcId: arc4.id, + arcOrder: 4, + zone: null, + minLevel: 16, + repeatable: false, + }, + { + name: 'Le Gardien du Passage', + description: 'Au cœur du Ruisseau, un dernier gardien bloque le chemin. Il ne vous laissera passer que si vous vous êtes acceptés.', + acceptText: 'Une forme massive se dresse dans l\'eau cristalline. Le Gardien du Reflet. Il est fait de tous les doutes que vous avez laissés derrière vous, condensés en une seule entité.\nIl ne parle pas. Il attend.', + completeText: 'Le Gardien s\'effrite comme du verre au soleil. Un éclat tombe dans l\'eau — un Éclat de Miroir qui chante doucement.\nVous l\'avez. Un Fragment du Chant Perdu.\nL\'eau s\'opacifie, l\'air se rafraîchit. Le Miroir est passé. Le courant vous appelle plus loin.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Gardien du Reflet') ?? null, + objectiveCount: 1, + rewardXp: 1200, + rewardGold: 500, + rewardTitle: 'Vainqueur du Miroir', + arcId: arc4.id, + arcOrder: 5, + zone: 'ruisseau_miroir', + minLevel: 17, + repeatable: false, + }, + ]; + + // ═══════════════════════════════════════════════════ + // ARC 5 — LE MARAIS DES MURMURES (ch.10) + // "L'eau retient les souvenirs et les murmure." + // ═══════════════════════════════════════════════════ + + let arc5 = await arcRepo.findOne({ where: { name: 'Le Marais des Murmures' } }); + if (!arc5) { + arc5 = await arcRepo.save(arcRepo.create({ + name: 'Le Marais des Murmures', + description: 'Un marais oppressant où l\'eau chuchote des vérités anciennes. Une sage vous y attend.', + zone: 'marais_murmures', + sortOrder: 5, + minLevel: 18, + })); + } + + const arc5Quests = [ + { + name: 'Les Voix dans la Brume', + description: 'Le marais murmure. Des voix émergent du courant lui-même, portant des bribes de mots et de souvenirs.', + acceptText: 'Mira s\'arrête, l\'oreille tendue : « Entendez-vous… ces voix ? »\nVell fronce les sourcils : « C\'est le vent. Des sifflements, rien de plus. »\nMais vous, vous n\'écoutez pas avec vos oreilles. Vous percevez quelque chose de plus profond.\nLes spectres du marais errent entre les arbres morts. Dissipez-les pour avancer.', + completeText: 'Les spectres se dissipent en murmures. Leurs dernières paroles flottent dans l\'air comme un écho de quelque chose de très ancien.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Spectre de Brume') ?? null, + objectiveCount: 3, + rewardXp: 600, + rewardGold: 250, + rewardTitle: null as string | null, + arcId: arc5.id, + arcOrder: 1, + zone: 'marais_murmures', + minLevel: 18, + repeatable: false, + }, + { + name: 'La Batracienne', + description: 'Dans une clairière aquatique, une forme ancienne vous attend. Elle savait que vous viendriez.', + acceptText: 'Dans une clairière, vous voyez une forme. Une batracienne ancienne, vêtue de lianes et d\'algues, aux yeux voilés mais brillants d\'une lumière verte.\n« Bienvenue, voyageurs. Le Marais vous attendait. »\nVous sentez une vibration dans l\'eau, comme une reconnaissance.\n« Tu portes le chant en germe, petit têtard. Mais avant la source, il faut comprendre le courant. Veux-tu entendre ? »', + completeText: 'Vous acquiescez. La Batracienne pose une patte sur votre front, et l\'eau se trouble autour de vous...', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 500, + rewardGold: 200, + rewardTitle: null, + arcId: arc5.id, + arcOrder: 2, + zone: null as string | null, + minLevel: 19, + repeatable: false, + }, + { + name: 'La Vision', + description: 'La Batracienne vous montre le passé. Vous voyez l\'Hydre... telle qu\'elle était avant.', + acceptText: 'Vision.\nDes étangs anciens, des batraciens chantant sous une lune violette. L\'Hydre, paisible autrefois, gardienne des eaux profondes.\nPuis la rupture : un chant dévoyé, une note brisée, la douleur de l\'Hydre devenue chaos.\nVous rouvrez les yeux, bouleversé.\n« L\'Hydre était la clef… Elle est la mémoire déformée. Nous devons la guérir, pas la fuir. »', + completeText: 'La Batracienne hoche la tête : « Tu entends. Le chant te choisira, si tu choisis de l\'écouter. »\nDans l\'air, une note pure monte. Un fragment du chant, fragile mais réel. Vous l\'accueillez, le gravez en vous.', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 800, + rewardGold: 350, + rewardTitle: null, + arcId: arc5.id, + arcOrder: 3, + zone: null, + minLevel: 19, + repeatable: false, + }, + { + name: 'La Mémoire de l\'Hydre', + description: 'Un écho de l\'Hydre hante le marais. Ce n\'est pas l\'Hydre elle-même — c\'est sa douleur, incarnée.', + acceptText: 'Vell murmure : « Ce que tu as vu, Tetardtek… qu\'est-ce que cela signifie pour nous ? »\n« L\'Hydre n\'est pas qu\'un monstre. Elle était la gardienne d\'un équilibre que le chant maintenait. Quand le chant a été brisé, elle a sombré. »\nMira : « Gorn aurait dit que toute chose brisée peut être transformée. »\nUne ombre se dresse devant vous — la Mémoire de l\'Hydre. Sa douleur faite chair.', + completeText: 'La Mémoire se dissipe en un soupir. Une fiole de brume reste — la Brume Condensée, dernier écho du chant de l\'Hydre dans ce marais.\nVell sourit légèrement : « Gorn aurait ajouté : "l\'eau qui dort peut devenir torrent." »\nVous riez doucement, unis par le souvenir.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Mémoire de l\'Hydre') ?? null, + objectiveCount: 1, + rewardXp: 1500, + rewardGold: 600, + rewardTitle: 'Porteur de Vérité', + arcId: arc5.id, + arcOrder: 4, + zone: 'marais_murmures', + minLevel: 20, + repeatable: false, + }, + ]; + + // ═══════════════════════════════════════════════════ + // ARC 6 — LE TORRENT BRISÉ (ch.11) + // "La force brute ne passe pas." + // ═══════════════════════════════════════════════════ + + let arc6 = await arcRepo.findOne({ where: { name: 'Le Torrent Brisé' } }); + if (!arc6) { + arc6 = await arcRepo.save(arcRepo.create({ + name: 'Le Torrent Brisé', + description: 'Des eaux violentes, des rochers acérés. Vell doit apprendre sa leçon la plus dure.', + zone: 'torrent_brise', + sortOrder: 6, + minLevel: 21, + })); + } + + const arc6Quests = [ + { + name: 'Le Courant Furieux', + description: 'Le torrent gronde. Des élémentaux de remous protègent le passage.', + acceptText: 'Le paysage change, devenant plus escarpé, plus sauvage. Des rochers acérés fendent l\'onde, et l\'eau gronde avec violence.\nVell observe le flot tumultueux, son regard s\'allumant d\'un feu ancien : « Laissez-moi ouvrir la voie. »\nIl s\'élance, défiant les remous... mais le torrent ne se laisse pas dompter.', + completeText: 'Les élémentaux reculent, mais Vell halète. L\'eau résiste à chaque poussée. La force brute ne suffira pas ici.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Élémental de Remous') ?? null, + objectiveCount: 2, + rewardXp: 800, + rewardGold: 350, + rewardTitle: null as string | null, + arcId: arc6.id, + arcOrder: 1, + zone: 'torrent_brise', + minLevel: 21, + repeatable: false, + }, + { + name: 'La Chute de Vell', + description: 'Vell s\'obstine et le torrent le projette contre un rocher. Dans l\'eau noire, il entend une voix.', + acceptText: 'Vell rugit, frappe l\'eau : « Je dois réussir ! Je dois être assez fort ! Sinon… sinon Gorn est parti pour rien. »\nMais le torrent, indifférent, le projette contre un rocher. Étourdi, il sombre.\nLà, dans la noirceur du courant, une voix : « Crois... ou coule. »\nVous devez le repérer et le tirer vers la rive.', + completeText: 'Mira tend la nageoire, chantant une note pure. La torpeur cède.\nVell haletait, brisé, non de blessures, mais d\'orgueil.\n« J\'ai échoué... encore. J\'ai laissé Gorn partir... sans rien apprendre. »\nMira s\'approche : « Tu as trop voulu porter seul. Le courant ne se force pas, Vell. Il se suit, en confiance. »', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Roc Vivant') ?? null, + objectiveCount: 1, + rewardXp: 1000, + rewardGold: 400, + rewardTitle: null, + arcId: arc6.id, + arcOrder: 2, + zone: 'torrent_brise', + minLevel: 22, + repeatable: false, + }, + { + name: 'La Voie Étroite', + description: 'Mira perçoit un chemin invisible. Ensemble, vous pouvez traverser.', + acceptText: 'Mira lève les yeux. Elle perçoit une veine plus douce, une trajectoire étroite, où l\'eau glisse sans heurt.\n« Là. Nous passerons ensemble. »\nGuidés par elle, vous vous laissez porter. Tetardtek fredonne le fragment du chant. Mira lit les remous. Vell nage entre vous, apaisé.\nLe torrent, sévère mais pas cruel, vous accueille.', + completeText: 'Quand vous atteignez l\'autre rive, Vell regarde en arrière. Il comprend.\n« La vraie force... c\'est de ne pas lutter seul. C\'est de croire. Nous sommes un flot, pas des gouttes isolées. »\nUn fragment de confiance naît en lui, pur et solide.', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 600, + rewardGold: 300, + rewardTitle: null, + arcId: arc6.id, + arcOrder: 3, + zone: null as string | null, + minLevel: 22, + repeatable: false, + }, + { + name: 'La Cascade Éveillée', + description: 'Une cascade souterraine bloque le passage. La force brute est inutile. Seule la résonance peut l\'ouvrir.', + acceptText: 'Vell s\'assit dans l\'eau, à l\'écoute. Il perçoit le rythme, la pulsation, la faille.\nUn souvenir de Gorn résonne : « Le roc le plus solide est celui qui a dansé avec l\'eau. »\nVell se laisse aller. En vibrant avec l\'onde, il amplifie le courant.\nMais la Cascade ne se laissera pas ouvrir sans combat.', + completeText: 'Les roches cèdent, l\'eau s\'ouvre. Vell a trouvé sa force : maîtrise, patience, résonance.\nIl n\'est plus une force brute. Il est un écho, une réponse.\nVous émergez transformés, épuisés mais éveillés. L\'eau vibre autour de vous, vous reconnaissant comme héritiers du chant.', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Cascade Éveillée') ?? null, + objectiveCount: 1, + rewardXp: 2000, + rewardGold: 800, + rewardTitle: 'Maître du Torrent', + arcId: arc6.id, + arcOrder: 4, + zone: 'torrent_brise', + minLevel: 23, + repeatable: false, + }, + ]; + + // ═══════════════════════════════════════════════════ + // ARC 7 — LA SOURCE DU COURANT (ch.12) + // "Le lieu légendaire où le Chant est né." + // ═══════════════════════════════════════════════════ + + let arc7 = await arcRepo.findOne({ where: { name: 'La Source du Courant' } }); + if (!arc7) { + arc7 = await arcRepo.save(arcRepo.create({ + name: 'La Source du Courant', + description: 'Le courant vous guide vers des terres inconnues où la lumière devient plus claire, presque éthérée.', + zone: 'source_courant', + sortOrder: 7, + minLevel: 24, + })); + } + + const arc7Quests = [ + { + name: 'Les Gardiens Sacrés', + description: 'La Source est protégée. Des gardiens anciens testent ceux qui s\'approchent.', + acceptText: 'Le sol change, les pierres deviennent translucides, le courant chante. Vous approchez d\'un lieu légendaire.\nDevant vous, des formes lumineuses se matérialisent — les Gardiens de la Vasque. Ils ne sont pas hostiles. Ils évaluent.', + completeText: 'Les Gardiens s\'écartent. Vous avez été jugés dignes. La vasque de pierre apparaît, creusée par le temps, où l\'eau jaillit en fils de lumière.\nMira recule, les yeux écarquillés : « C\'est... magnifique. »', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Gardien de la Vasque') ?? null, + objectiveCount: 2, + rewardXp: 1200, + rewardGold: 500, + rewardTitle: null as string | null, + arcId: arc7.id, + arcOrder: 1, + zone: 'source_courant', + minLevel: 24, + repeatable: false, + }, + { + name: 'Les Trois Visions', + description: 'La Source vous enveloppe. Chacun de vous reçoit une vision.', + acceptText: 'La Source vibre. Trois vagues s\'en détachent, vous enveloppant.\nVous voyez l\'étang renaître, inondé de chant. Gorn est là, devenu grenouille à la peau d\'argent, le regard paisible. Il murmure : « Le courant ne se possède pas. Il se transmet. Porte-le, et fais-le vivre. »\nMira flotte dans une eau sans fin, son chant se mêlant à celui de milliers de voix.\nVell se voit face à lui-même, plus grand, plus calme. Une force tranquille.', + completeText: 'Quand les vagues se dissipent, vous vous regardez. Transformés.\nDans la vasque, un filament de lumière violette s\'élève, spiralant dans l\'air, se divisant en trois. Il entre en vous, scellant votre lien au chant.\nVous comprenez, sans mots, que vous êtes désormais capables de cultiver le Dao du Courant.', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 1500, + rewardGold: 600, + rewardTitle: null, + arcId: arc7.id, + arcOrder: 2, + zone: null as string | null, + minLevel: 25, + repeatable: false, + }, + { + name: 'L\'Esprit du Chant', + description: 'Un dernier esprit erre près de la vasque. Il porte le fragment final — celui que Gorn n\'a jamais pu atteindre.', + acceptText: 'Tetardtek tend une nageoire : l\'eau y répond, traçant un cercle lumineux.\nMira chante doucement : les remous s\'harmonisent autour d\'elle.\nVell ferme les yeux, et une onde de force pulse dans l\'eau.\nMais quelque chose résiste. L\'Esprit du Chant, dernier écho de la mélodie perdue, ne vous laissera partir qu\'avec sa bénédiction.', + completeText: 'L\'Esprit se dissipe en une note pure qui résonne longtemps dans la vasque. Le Fragment du Chant se dépose entre vos nageoires.\nVous fermez les yeux. Vous ressentez tout : la Source, l\'étang, l\'Hydre. Tout est relié.\n« Nous sommes prêts. »', + objectiveType: 'kill_monster', + objectiveTargetId: m.get('Esprit du Chant') ?? null, + objectiveCount: 1, + rewardXp: 2000, + rewardGold: 800, + rewardTitle: null, + arcId: arc7.id, + arcOrder: 3, + zone: 'source_courant', + minLevel: 25, + repeatable: false, + }, + { + name: 'L\'Héritage du Courant', + description: 'Le Dao du Courant coule en vous. Le chant est presque complet. L\'étang vous attend.', + acceptText: 'L\'eau vibre, et le courant vous pousse doucement vers l\'aval.\nMira : « Ce n\'est pas fini. L\'Hydre souffre encore. Et maintenant, nous avons le pouvoir de la guérir. »\nVell, le regard posé vers l\'horizon : « Si le courant nous appelle, nous devons répondre. »\nLe retour commence. L\'Hydre attend. Et le Chant attend d\'être chanté.', + completeText: 'Vous portez le Dao du Courant. Les fragments du Chant vibrent en vous.\nL\'heure du retour a sonné. L\'étang, brisé, attend sa guérison.\nEt l\'Hydre aussi.\n\n— L\'Odyssée continue...', + objectiveType: 'story_event', + objectiveTargetId: null, + objectiveCount: 1, + rewardXp: 2500, + rewardGold: 1000, + rewardTitle: 'Héritier du Chant', + arcId: arc7.id, + arcOrder: 4, + zone: null, + minLevel: 25, + repeatable: false, + }, + ]; + + // ═══════════════════════════════════════════════════ + // SEED + // ═══════════════════════════════════════════════════ + + const allQuests = [...arc4Quests, ...arc5Quests, ...arc6Quests, ...arc7Quests]; + + for (const q of allQuests) { + const existing = await questRepo.findOne({ where: { name: q.name } }); + if (!existing) { + await questRepo.save(questRepo.create(q)); + questsAdded++; + } + } + + // Quêtes répétables (grind léger entre les arcs — optionnel, pas obligatoire pour l'histoire) + const dailyQuests = [ + { name: 'Éclats quotidiens', description: 'Récoltez des Éclats de Miroir dans le Ruisseau.', objectiveType: 'gather_material', objectiveTargetId: mat.get('Éclat de Miroir'), objectiveCount: 3, rewardXp: 300, rewardGold: 150, zone: 'ruisseau_miroir', minLevel: 16 }, + { name: 'Brumes du jour', description: 'Récoltez de la Mousse Murmurante dans le Marais.', objectiveType: 'gather_material', objectiveTargetId: mat.get('Mousse Murmurante'), objectiveCount: 2, rewardXp: 400, rewardGold: 200, zone: 'marais_murmures', minLevel: 19 }, + { name: 'Pierres du Torrent', description: 'Récoltez des Pierres de Torrent.', objectiveType: 'gather_material', objectiveTargetId: mat.get('Pierre de Torrent'), objectiveCount: 2, rewardXp: 500, rewardGold: 250, zone: 'torrent_brise', minLevel: 22 }, + ]; + + for (const q of dailyQuests) { + const existing = await questRepo.findOne({ where: { name: q.name } }); + if (!existing) { + await questRepo.save(questRepo.create({ ...q, rewardTitle: null, arcId: null, arcOrder: 0, repeatable: true, acceptText: null, completeText: null })); + questsAdded++; + } + } + + console.log(`✅ Odyssée: ${questsAdded} quêtes (4 arcs narratifs + ${dailyQuests.length} répétables)`); +} diff --git a/src/database/odyssee-seed.ts b/src/database/odyssee-seed.ts new file mode 100644 index 0000000..174511d --- /dev/null +++ b/src/database/odyssee-seed.ts @@ -0,0 +1,298 @@ +import { DataSource } from 'typeorm'; + +/** + * Seed Phase 3 — L'Odyssée d'un têtard + * Zones 4-7 : Ruisseau Miroir, Marais des Murmures, Torrent Brisé, Source du Courant + * Monstres, items, matériaux, NPCs + */ + +// ── MONSTRES ── + +const MONSTERS = [ + // Ruisseau Miroir (niv 13-15) — ennemis = reflets, illusions, créatures de cristal + // Commence pile après le Sphinx (niv 12-15) — le joueur est ~niv 13 + { name: 'Reflet Sombre', zone: 'ruisseau_miroir', minLevel: 12, maxLevel: 14, hp: 250, attack: 28, defense: 12, attackType: 'magic', xpReward: 95, goldMin: 55, goldMax: 130, dropMaterialId: null }, + { name: 'Gerris de Cristal', zone: 'ruisseau_miroir', minLevel: 13, maxLevel: 15, hp: 200, attack: 32, defense: 8, attackType: 'ranged', xpReward: 105, goldMin: 60, goldMax: 140, dropMaterialId: null }, + { name: 'Miroir Brisé', zone: 'ruisseau_miroir', minLevel: 13, maxLevel: 15, hp: 280, attack: 26, defense: 16, attackType: 'magic', xpReward: 115, goldMin: 65, goldMax: 150, dropMaterialId: null }, + { name: 'Écho de Doute', zone: 'ruisseau_miroir', minLevel: 14, maxLevel: 16, hp: 320, attack: 30, defense: 14, attackType: 'magic', xpReward: 130, goldMin: 70, goldMax: 165, dropMaterialId: null }, + { name: 'Gardien du Reflet', zone: 'ruisseau_miroir', minLevel: 14, maxLevel: 17, hp: 450, attack: 36, defense: 18, attackType: 'magic', xpReward: 180, goldMin: 90, goldMax: 220, dropMaterialId: null }, + + // Marais des Murmures (niv 16-18) — spectres, brume, mémoire + { name: 'Spectre de Brume', zone: 'marais_murmures', minLevel: 15, maxLevel: 17, hp: 350, attack: 34, defense: 10, attackType: 'magic', xpReward: 140, goldMin: 75, goldMax: 180, dropMaterialId: null }, + { name: 'Crapaud Ancien', zone: 'marais_murmures', minLevel: 16, maxLevel: 18, hp: 400, attack: 30, defense: 18, attackType: 'melee', xpReward: 155, goldMin: 80, goldMax: 195, dropMaterialId: null }, + { name: 'Murmure Incarné', zone: 'marais_murmures', minLevel: 16, maxLevel: 18, hp: 380, attack: 38, defense: 12, attackType: 'magic', xpReward: 165, goldMin: 85, goldMax: 210, dropMaterialId: null }, + { name: 'Liane Étrangleuse', zone: 'marais_murmures', minLevel: 17, maxLevel: 19, hp: 440, attack: 32, defense: 20, attackType: 'melee', xpReward: 175, goldMin: 90, goldMax: 225, dropMaterialId: null }, + { name: 'Mémoire de l\'Hydre', zone: 'marais_murmures', minLevel: 17, maxLevel: 20, hp: 580, attack: 42, defense: 22, attackType: 'magic', xpReward: 240, goldMin: 120, goldMax: 300, dropMaterialId: null }, + + // Torrent Brisé (niv 19-21) — élémentaux d'eau, force brute, courant violent + { name: 'Élémental de Remous', zone: 'torrent_brise', minLevel: 18, maxLevel: 20, hp: 480, attack: 40, defense: 16, attackType: 'melee', xpReward: 195, goldMin: 100, goldMax: 250, dropMaterialId: null }, + { name: 'Rapide d\'Écume', zone: 'torrent_brise', minLevel: 19, maxLevel: 21, hp: 380, attack: 46, defense: 10, attackType: 'ranged', xpReward: 210, goldMin: 105, goldMax: 265, dropMaterialId: null }, + { name: 'Roc Vivant', zone: 'torrent_brise', minLevel: 19, maxLevel: 21, hp: 600, attack: 36, defense: 28, attackType: 'melee', xpReward: 225, goldMin: 110, goldMax: 280, dropMaterialId: null }, + { name: 'Tourbillon Furieux', zone: 'torrent_brise', minLevel: 20, maxLevel: 22, hp: 520, attack: 44, defense: 18, attackType: 'magic', xpReward: 240, goldMin: 115, goldMax: 300, dropMaterialId: null }, + { name: 'Cascade Éveillée', zone: 'torrent_brise', minLevel: 20, maxLevel: 23, hp: 720, attack: 50, defense: 24, attackType: 'melee', xpReward: 320, goldMin: 150, goldMax: 400, dropMaterialId: null }, + + // Source du Courant (niv 22-25) — gardiens sacrés, lumière, épreuves + { name: 'Gardien de la Vasque', zone: 'source_courant', minLevel: 21, maxLevel: 23, hp: 650, attack: 48, defense: 22, attackType: 'magic', xpReward: 280, goldMin: 140, goldMax: 350, dropMaterialId: null }, + { name: 'Onde Sentinelle', zone: 'source_courant', minLevel: 22, maxLevel: 24, hp: 550, attack: 52, defense: 18, attackType: 'ranged', xpReward: 300, goldMin: 150, goldMax: 380, dropMaterialId: null }, + { name: 'Esprit du Chant', zone: 'source_courant', minLevel: 22, maxLevel: 25, hp: 700, attack: 46, defense: 26, attackType: 'magic', xpReward: 330, goldMin: 160, goldMax: 420, dropMaterialId: null }, + { name: 'Prisme Ancien', zone: 'source_courant', minLevel: 23, maxLevel: 25, hp: 800, attack: 54, defense: 24, attackType: 'magic', xpReward: 360, goldMin: 180, goldMax: 460, dropMaterialId: null }, + { name: 'Avatar du Courant', zone: 'source_courant', minLevel: 23, maxLevel: 25, hp: 1000, attack: 60, defense: 30, attackType: 'magic', xpReward: 500, goldMin: 250, goldMax: 600, dropMaterialId: null }, +]; + +// ── MATÉRIAUX ── + +const MATERIALS = [ + // Ruisseau Miroir + { name: 'Éclat de Miroir', rarity: 'rare', description: 'Fragment de verre cristallin tombé du Ruisseau. Reflète une lumière étrange.' }, + { name: 'Larme de Cristal', rarity: 'rare', description: 'Goutte d\'eau solidifiée. On dit qu\'elle contient un souvenir.' }, + { name: 'Essence de Reflet', rarity: 'epic', description: 'L\'âme d\'un reflet vaincu. Vibre doucement entre les doigts.' }, + + // Marais des Murmures + { name: 'Mousse Murmurante', rarity: 'rare', description: 'Mousse qui chuchote quand on l\'approche de l\'oreille.' }, + { name: 'Racine de Mémoire', rarity: 'rare', description: 'Racine noueuse imprégnée des souvenirs du marais.' }, + { name: 'Brume Condensée', rarity: 'epic', description: 'Fiole de brume pure. Les murmures y sont encore audibles.' }, + + // Torrent Brisé + { name: 'Pierre de Torrent', rarity: 'rare', description: 'Roche polie par des siècles de courant furieux.' }, + { name: 'Écume Solidifiée', rarity: 'rare', description: 'Écume blanche durcie par la force du torrent.' }, + { name: 'Cœur de Cascade', rarity: 'epic', description: 'Noyau cristallin arraché au cœur de la cascade éveillée.' }, + + // Source du Courant + { name: 'Eau de la Source', rarity: 'epic', description: 'Eau pure de la Source. Brille d\'une lumière douce.' }, + { name: 'Fragment du Chant', rarity: 'legendary', description: 'Morceau de la mélodie perdue. Vibre d\'une harmonie ancienne.' }, + { name: 'Filament Violet', rarity: 'legendary', description: 'Fil de lumière violette — le lien du Dao du Courant.' }, +]; + +// ── ITEMS ── + +const ITEMS = [ + // Ruisseau Miroir (niv 16-18) + { name: 'Lame du Reflet', type: 'weapon', rarity: 'epic', attackBonus: 30, defenseBonus: 0, buyPrice: 2000, minLevel: 13, zone: 'ruisseau_miroir', description: 'Une épée qui montre la vérité à celui qu\'elle frappe.' }, + { name: 'Bouclier Miroir', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 22, buyPrice: 1800, minLevel: 13, zone: 'ruisseau_miroir', description: 'Renvoie un reflet déformé des attaques.' }, + { name: 'Dague de Cristal', type: 'weapon', rarity: 'rare', attackBonus: 26, defenseBonus: 0, buyPrice: 1500, minLevel: 12, zone: 'ruisseau_miroir', description: 'Transparente et tranchante comme un reproche.' }, + + // Marais des Murmures (niv 19-21) + { name: 'Bâton des Murmures', type: 'weapon', rarity: 'epic', attackBonus: 34, defenseBonus: 0, intelligenceBonus: 5, buyPrice: 2800, minLevel: 16, zone: 'marais_murmures', description: 'Les murmures du marais guident chaque coup.' }, + { name: 'Robe de Brume', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 26, buyPrice: 2500, minLevel: 16, zone: 'marais_murmures', description: 'Tissée de brume vivante. Dissimule et protège.' }, + { name: 'Amulette de la Batracienne', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 20, intelligenceBonus: 8, vitaliteBonus: 3, buyPrice: 3200, minLevel: 17, zone: 'marais_murmures', description: 'Don de la sage du marais. Pulse de lumière verte.' }, + + // Torrent Brisé (niv 22-24) + { name: 'Masse du Torrent', type: 'weapon', rarity: 'epic', attackBonus: 40, defenseBonus: 0, forceBonus: 5, buyPrice: 3800, minLevel: 19, zone: 'torrent_brise', description: 'Lourde comme le courant. Chaque coup résonne.' }, + { name: 'Armure d\'Écume', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 30, buyPrice: 3500, minLevel: 19, zone: 'torrent_brise', description: 'L\'écume durcie absorbe les chocs comme l\'eau absorbe la pierre.' }, + { name: 'Gantelet de Résonance', type: 'armor', rarity: 'epic', attackBonus: 5, defenseBonus: 24, forceBonus: 6, buyPrice: 4000, minLevel: 20, zone: 'torrent_brise', description: 'Amplifie la force par l\'harmonie. Forgé dans le torrent.' }, + + // Source du Courant (niv 25+) + { name: 'Lame de la Source', type: 'weapon', rarity: 'legendary', attackBonus: 50, defenseBonus: 0, intelligenceBonus: 8, chanceBonus: 5, buyPrice: 6000, minLevel: 22, zone: 'source_courant', description: 'Forgée dans l\'eau de la Source. Le Chant résonne à chaque frappe.' }, + { name: 'Armure du Dao', type: 'armor', rarity: 'legendary', attackBonus: 0, defenseBonus: 38, vitaliteBonus: 10, buyPrice: 5500, minLevel: 22, zone: 'source_courant', description: 'Imprégnée du Courant lui-même. Protège le corps et l\'esprit.' }, + { name: 'Bâton de Gorn', type: 'weapon', rarity: 'legendary', attackBonus: 45, defenseBonus: 5, intelligenceBonus: 12, buyPrice: 7000, minLevel: 22, zone: 'source_courant', description: 'L\'héritage de l\'ancien. Brille d\'une lumière dorée quand le Chant résonne.' }, + + // Potions avancées + { name: 'Élixir du Courant', type: 'consumable', rarity: 'epic', attackBonus: 0, defenseBonus: 0, forceBonus: 100, buyPrice: 80, minLevel: 13, zone: null, description: 'Restaure 100 points d\'endurance. L\'eau pure revitalise.' }, + { name: 'Potion de soin majeure', type: 'consumable', rarity: 'epic', attackBonus: 0, defenseBonus: 0, forceBonus: 0, buyPrice: 60, minLevel: 13, zone: null, description: 'Restaure 80% des PV.' }, +]; + +// ── NPCs ── + +const NPCS = [ + // Village — PNJ permanents + { + name: 'Gorn', + role: 'mentor', + location: 'village_quests', + description: 'L\'ancien de l\'étang. Sage, grave, bienveillant. Porte le poids du savoir.', + lore: 'On dit que Gorn a effleuré la Pierre-Mémoire et n\'a jamais été le même depuis.', + spriteKey: 'npc_gorn', + minLevel: 1, + maxLevel: 15, // Disparaît après le sacrifice (ch.7) + dialogues: [ + { trigger: 'default', text: 'Bienvenue, jeune têtard. Le courant te porte ici pour une raison.', action: 'open_quests' }, + { trigger: 'level_3', text: 'Tu as grandi. Le courant murmure ton nom, l\'entends-tu ?', action: 'open_quests' }, + { trigger: 'level_5', text: 'Le Dao du Courant... peu en parlent, peu en savent la voie. Mais toi, tu commences à comprendre.', action: 'open_quests' }, + { trigger: 'level_10', text: 'Viens. Il est temps que je te montre la Pierre-Mémoire. Ce que peu ont vu.', action: 'open_quests' }, + { trigger: 'arc_completed:egouts', text: 'Tu descends de plus en plus profond. Bientôt, tu verras ce que moi-même j\'ai tenté d\'oublier.', action: 'open_quests' }, + ], + }, + { + name: 'Pierre-Mémoire', + role: 'quest_giver', + location: 'village_quests', + description: 'Pierre ancienne gravée de symboles. Les échos de Gorn y résonnent encore.', + lore: 'La Pierre-Mémoire existe depuis avant l\'étang. Elle porte les souvenirs de ceux qui l\'ont touchée.', + spriteKey: 'npc_pierre_memoire', + minLevel: 16, // Apparaît quand Gorn disparaît + maxLevel: null, + dialogues: [ + { trigger: 'default', text: '« Le courant t\'attend. Va. » — les mots de Gorn résonnent dans la pierre.', action: 'open_quests' }, + { trigger: 'level_20', text: 'La pierre pulse. Des visions anciennes dansent sur sa surface — des batraciens chantant sous une lune violette.', action: 'open_quests' }, + { trigger: 'level_25', text: 'La Pierre-Mémoire brille intensément. Le Chant complet est presque là. « Continue. »', action: 'open_quests' }, + ], + }, + { + name: 'Mira', + role: 'companion', + location: 'village_plaza', + description: 'Douce, intuitive, silencieusement puissante. Connectée au Courant sans le savoir.', + lore: 'Mira fredonne des mélodies aquatiques. L\'eau semble la porter, la guider, la compléter.', + spriteKey: 'npc_mira', + minLevel: 1, + maxLevel: null, + dialogues: [ + { trigger: 'default', text: 'Salut ! Tu as vu les nénuphars aujourd\'hui ? Ils brillent autrement...', action: 'heal' }, + { trigger: 'level_5', text: 'Je ne sais pas pourquoi, mais quand je nage, l\'eau me parle. Tu crois que c\'est normal ?', action: 'heal' }, + { trigger: 'level_10', text: 'Le chant guérit. Mais il faut l\'entendre. Et l\'accepter.', action: 'heal' }, + { trigger: 'arc_completed:desert', text: 'L\'étang souffre. Je le sens dans chaque remous. On doit faire quelque chose.', action: 'heal' }, + { trigger: 'level_16', text: 'Le Ruisseau Miroir m\'effraie. Il montre ce qu\'on ne veut pas voir... mais je viendrai avec toi.', action: 'heal' }, + { trigger: 'level_20', text: 'J\'ai accepté mon chant, Tetardtek. Et ensemble, nous sommes plus que nos reflets.', action: 'heal' }, + { trigger: 'level_25', text: 'La Source nous attend. Je sens le Chant, complet, qui vibre dans l\'eau. Allons-y.', action: 'heal' }, + ], + }, + { + name: 'Vell', + role: 'rival', + location: 'village_arena', + description: 'Impétueux, fier, loyal. Confond d\'abord force et valeur.', + lore: 'Vell ne pense qu\'à défier ses compagnons en vitesse. Sa force physique est indéniable.', + spriteKey: 'npc_vell', + minLevel: 1, + maxLevel: null, + dialogues: [ + { trigger: 'default', text: 'Eh ! On fait la course ? Je te laisse même de l\'avance.', action: 'challenge' }, + { trigger: 'level_5', text: 'Tu t\'es laissé porter par quelque chose à la course. Ce n\'était pas de la vitesse... c\'était autre chose.', action: 'challenge' }, + { trigger: 'level_10', text: 'J\'étais fort, mais je n\'ai rien pu faire quand ça a compté. Je dois comprendre la vraie force.', action: 'challenge' }, + { trigger: 'arc_completed:desert', text: 'L\'étang est brisé. Si ma force ne peut pas protéger, à quoi sert-elle ? ...Je viens avec toi.', action: 'challenge' }, + { trigger: 'level_20', text: 'Le Torrent m\'a montré. La vraie force, c\'est de ne pas lutter seul.', action: 'challenge' }, + { trigger: 'level_25', text: 'Nous sommes un flot, pas des gouttes isolées. Allons restaurer le Chant.', action: 'challenge' }, + ], + }, + + // PNJ de zone + { + name: 'La Batracienne', + role: 'sage', + location: 'marais_murmures', + description: 'Sage ancienne vêtue de lianes et d\'algues. Ses yeux voilés voient plus loin que les nôtres.', + lore: 'Elle vit dans le Marais depuis des générations. On dit qu\'elle a vu le Chant originel.', + spriteKey: 'npc_batracienne', + minLevel: 19, + maxLevel: null, + dialogues: [ + { trigger: 'default', text: 'Bienvenue, voyageurs. Le Marais vous attendait.', action: 'open_quests' }, + { trigger: 'level_20', text: 'Tu portes le chant en germe, petit têtard. Mais avant la source, il faut comprendre le courant.', action: 'open_quests' }, + { trigger: 'level_22', text: 'L\'Hydre était la clef... Elle est la mémoire déformée. Vous devez la guérir, pas la fuir.', action: 'open_quests' }, + ], + }, + + // Marchands existants (formalisés comme PNJ) + { + name: 'Le Forgeron', + role: 'merchant', + location: 'village_forge', + description: 'Un vieux crapaud aux bras musclés. Son enclume résonne jour et nuit.', + lore: 'Il forge des armes depuis plus longtemps que quiconque se souvient.', + spriteKey: 'npc_forgeron', + minLevel: 1, + maxLevel: null, + dialogues: [ + { trigger: 'default', text: 'Apporte-moi des matériaux et je te forgerai quelque chose de solide.', action: 'open_forge' }, + { trigger: 'level_10', text: 'Tes armes grandissent avec toi. Bientôt tu auras besoin d\'acier des profondeurs.', action: 'open_forge' }, + { trigger: 'level_20', text: 'J\'ai entendu parler de matériaux dans le Torrent... si tu en rapportes, je pourrai forger du légendaire.', action: 'open_forge' }, + ], + }, + { + name: 'Le Marchand', + role: 'merchant', + location: 'village_shop', + description: 'Un triton jovial qui a toujours ce qu\'il faut.', + lore: 'Personne ne sait d\'où il vient. Ses marchandises changent avec les zones que vous explorez.', + spriteKey: 'npc_marchand', + minLevel: 1, + maxLevel: null, + dialogues: [ + { trigger: 'default', text: 'Bienvenue ! J\'ai de belles trouvailles aujourd\'hui.', action: 'open_shop' }, + { trigger: 'level_15', text: 'Les temps sont durs depuis l\'Hydre. Mais j\'ai de l\'équipement qui pourrait t\'aider au-delà de l\'étang.', action: 'open_shop' }, + { trigger: 'level_25', text: 'La Source... tu y vas vraiment ? Tiens, prends ça. Tu en auras besoin.', action: 'open_shop' }, + ], + }, +]; + +// ── SEED FUNCTION ── + +export async function seedOdyssee(dataSource: DataSource) { + let monstersAdded = 0; + let itemsAdded = 0; + let materialsAdded = 0; + let npcsAdded = 0; + + // --- Monstres --- + for (const m of MONSTERS) { + const existing = await dataSource.query('SELECT id FROM monsters WHERE name = ?', [m.name]); + if (existing.length === 0) { + await dataSource.query( + `INSERT INTO monsters (id, name, zone, min_level, max_level, hp, attack, defense, attack_type, xp_reward, gold_min, gold_max, drop_material_id) + VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [m.name, m.zone, m.minLevel, m.maxLevel, m.hp, m.attack, m.defense, m.attackType, m.xpReward, m.goldMin, m.goldMax, m.dropMaterialId], + ); + monstersAdded++; + } + } + + // --- Matériaux --- + const materialRepo = dataSource.getRepository('Material'); + const materialIdMap: Record = {}; + for (const mat of MATERIALS) { + let existing = await materialRepo.findOne({ where: { name: mat.name } }); + if (!existing) { + existing = await materialRepo.save(materialRepo.create(mat)); + materialsAdded++; + } + materialIdMap[mat.name] = existing.id; + } + + // Lier les drops aux monstres + const dropLinks: Record = { + 'Reflet Sombre': 'Éclat de Miroir', + 'Miroir Brisé': 'Larme de Cristal', + 'Gardien du Reflet': 'Essence de Reflet', + 'Spectre de Brume': 'Mousse Murmurante', + 'Murmure Incarné': 'Racine de Mémoire', + 'Mémoire de l\'Hydre': 'Brume Condensée', + 'Élémental de Remous': 'Pierre de Torrent', + 'Roc Vivant': 'Écume Solidifiée', + 'Cascade Éveillée': 'Cœur de Cascade', + 'Gardien de la Vasque': 'Eau de la Source', + 'Esprit du Chant': 'Fragment du Chant', + 'Avatar du Courant': 'Filament Violet', + }; + + for (const [monsterName, materialName] of Object.entries(dropLinks)) { + const matId = materialIdMap[materialName]; + if (matId) { + await dataSource.query( + 'UPDATE monsters SET drop_material_id = ? WHERE name = ?', + [matId, monsterName], + ); + } + } + + // --- Items --- + const itemRepo = dataSource.getRepository('Item'); + for (const item of ITEMS) { + const existing = await itemRepo.findOne({ where: { name: item.name } }); + if (!existing) { + await itemRepo.save(itemRepo.create(item)); + itemsAdded++; + } + } + + // --- NPCs --- + const npcRepo = dataSource.getRepository('Npc'); + for (const npc of NPCS) { + const existing = await npcRepo.findOne({ where: { name: npc.name } }); + if (!existing) { + await npcRepo.save(npcRepo.create(npc)); + npcsAdded++; + } + } + + console.log(`✅ Odyssée seed: ${monstersAdded} monstres, ${materialsAdded} matériaux, ${itemsAdded} items, ${npcsAdded} NPCs`); +} diff --git a/src/database/seed-odyssee.ts b/src/database/seed-odyssee.ts new file mode 100644 index 0000000..79302e6 --- /dev/null +++ b/src/database/seed-odyssee.ts @@ -0,0 +1,20 @@ +import 'reflect-metadata'; +import { AppDataSource } from './data-source'; +import { seedOdyssee } from './odyssee-seed'; +import { seedOdysseeQuests } from './odyssee-quests-seed'; + +async function seed() { + await AppDataSource.initialize(); + console.log('DB connectée (MySQL)'); + + await seedOdyssee(AppDataSource); + await seedOdysseeQuests(AppDataSource); + + await AppDataSource.destroy(); + console.log('✅ Odyssée seed terminé'); +} + +seed().catch((err) => { + console.error('Seed échoué :', err); + process.exit(1); +}); diff --git a/src/npc/npc.controller.ts b/src/npc/npc.controller.ts new file mode 100644 index 0000000..ce3483e --- /dev/null +++ b/src/npc/npc.controller.ts @@ -0,0 +1,23 @@ +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { NpcService } from './npc.service'; +import { AuthGuard } from '../auth/guards/auth.guard'; + +@Controller('api/npcs') +@UseGuards(AuthGuard) +export class NpcController { + constructor(private readonly npcService: NpcService) {} + + /** GET /api/npcs — tous les PNJ visibles pour le joueur */ + @Get() + async getAll(@Req() req: any) { + const { characterId, level } = req.character; + return this.npcService.getVisibleNpcs(characterId, level); + } + + /** GET /api/npcs?location=village_plaza — PNJ d'un emplacement */ + @Get('location') + async getByLocation(@Req() req: any, @Query('location') location: string) { + const { characterId, level } = req.character; + return this.npcService.getNpcsByLocation(characterId, level, location); + } +} diff --git a/src/npc/npc.entity.ts b/src/npc/npc.entity.ts new file mode 100644 index 0000000..fd4ba32 --- /dev/null +++ b/src/npc/npc.entity.ts @@ -0,0 +1,51 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +export type NpcRole = 'mentor' | 'companion' | 'merchant' | 'quest_giver' | 'sage' | 'rival'; + +@Entity('npcs') +export class Npc { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ length: 100 }) + name: string; + + @Column({ type: 'varchar', length: 20 }) + role: NpcRole; + + /** Emplacement dans le village ou zone du monde */ + @Column({ type: 'varchar', length: 50 }) + location: string; // 'village_plaza' | 'village_arena' | 'village_quests' | 'village_forge' | 'village_shop' | zone name + + @Column({ type: 'text', nullable: true }) + description: string | null; + + /** Lore courte affichée dans le hub */ + @Column({ type: 'text', nullable: true }) + lore: string | null; + + /** Niveau minimum du joueur pour voir ce PNJ */ + @Column({ name: 'min_level', default: 1 }) + minLevel: number; + + /** Niveau max de visibilité (null = toujours visible après minLevel) */ + @Column({ name: 'max_level', type: 'int', nullable: true }) + maxLevel: number | null; + + /** Clé sprite pour le frontend */ + @Column({ name: 'sprite_key', type: 'varchar', length: 50, nullable: true }) + spriteKey: string | null; + + /** Dialogues évolutifs — JSON array trié par priorité */ + @Column({ type: 'json', nullable: true }) + dialogues: NpcDialogue[] | null; +} + +export interface NpcDialogue { + /** Condition de déclenchement */ + trigger: string; // 'default' | 'level_5' | 'level_15' | 'arc_completed:desert' | 'story:gorn_sacrifice' | etc. + /** Texte affiché */ + text: string; + /** Action proposée (optionnel) */ + action?: string; // 'open_quests' | 'open_shop' | 'open_forge' | 'heal' | 'challenge' +} diff --git a/src/npc/npc.module.ts b/src/npc/npc.module.ts new file mode 100644 index 0000000..50afdf6 --- /dev/null +++ b/src/npc/npc.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Npc } from './npc.entity'; +import { NpcController } from './npc.controller'; +import { NpcService } from './npc.service'; +import { PlayerQuestArc } from '../quest/player-quest-arc.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Npc, PlayerQuestArc])], + controllers: [NpcController], + providers: [NpcService], + exports: [NpcService], +}) +export class NpcModule {} diff --git a/src/npc/npc.service.ts b/src/npc/npc.service.ts new file mode 100644 index 0000000..f44e3dd --- /dev/null +++ b/src/npc/npc.service.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThanOrEqual, MoreThanOrEqual, IsNull, Or } from 'typeorm'; +import { Npc, NpcDialogue } from './npc.entity'; +import { PlayerQuestArc } from '../quest/player-quest-arc.entity'; + +export interface NpcView { + id: string; + name: string; + role: string; + location: string; + description: string | null; + lore: string | null; + spriteKey: string | null; + dialogue: string; + action?: string; +} + +@Injectable() +export class NpcService { + constructor( + @InjectRepository(Npc) + private readonly npcRepo: Repository, + @InjectRepository(PlayerQuestArc) + private readonly playerArcRepo: Repository, + ) {} + + /** + * Retourne les PNJ visibles pour un joueur donné, avec le bon dialogue résolu. + */ + async getVisibleNpcs(characterId: string, playerLevel: number): Promise { + // Tous les PNJ dont le joueur est dans la fourchette de niveaux + const npcs = await this.npcRepo.find({ + where: { + minLevel: LessThanOrEqual(playerLevel), + }, + order: { location: 'ASC', name: 'ASC' }, + }); + + // Filtrer par maxLevel (null = pas de limite) + const visible = npcs.filter( + (npc) => npc.maxLevel === null || playerLevel <= npc.maxLevel, + ); + + // Arcs complétés par ce joueur (pour résoudre les dialogues conditionnels) + const completedArcs = await this.playerArcRepo.find({ + where: { characterId, completed: true }, + relations: ['questArc'], + }); + const completedArcZones = new Set( + completedArcs.map((pa) => pa.questArc?.zone).filter((z): z is string => z != null), + ); + + return visible.map((npc) => { + const resolved = this.resolveDialogue(npc.dialogues, playerLevel, completedArcZones); + return { + id: npc.id, + name: npc.name, + role: npc.role, + location: npc.location, + description: npc.description, + lore: npc.lore, + spriteKey: npc.spriteKey, + dialogue: resolved.text, + action: resolved.action, + }; + }); + } + + /** + * Retourne les PNJ d'un emplacement spécifique (ex: 'village_plaza') + */ + async getNpcsByLocation(characterId: string, playerLevel: number, location: string): Promise { + const all = await this.getVisibleNpcs(characterId, playerLevel); + return all.filter((npc) => npc.location === location); + } + + /** Résout le dialogue le plus pertinent selon l'état du joueur */ + private resolveDialogue( + dialogues: NpcDialogue[] | null, + playerLevel: number, + completedArcZones: Set, + ): { text: string; action?: string } { + if (!dialogues || dialogues.length === 0) { + return { text: '...' }; + } + + // Parcours par priorité (dernière condition valide gagne) + let best: NpcDialogue = dialogues[0]; + + for (const d of dialogues) { + if (d.trigger === 'default') { + // Default = fallback, déjà capturé + continue; + } + + // Trigger par niveau : "level_15" → joueur >= 15 + const levelMatch = d.trigger.match(/^level_(\d+)$/); + if (levelMatch && playerLevel >= parseInt(levelMatch[1], 10)) { + best = d; + continue; + } + + // Trigger par arc complété : "arc_completed:desert" + const arcMatch = d.trigger.match(/^arc_completed:(.+)$/); + if (arcMatch && completedArcZones.has(arcMatch[1])) { + best = d; + continue; + } + + // Trigger story (future — quand on aura un story tracker) + // Pour l'instant, les triggers story sont ignorés + } + + return { text: best.text, action: best.action }; + } +} diff --git a/src/quest/quest.entity.ts b/src/quest/quest.entity.ts index d9a132d..932985d 100644 --- a/src/quest/quest.entity.ts +++ b/src/quest/quest.entity.ts @@ -19,9 +19,17 @@ export class Quest { @Column('text') description: string; + /** Texte narratif affiché quand le joueur accepte la quête (le PNJ parle) */ + @Column({ name: 'accept_text', type: 'text', nullable: true }) + acceptText: string | null; + + /** Texte narratif affiché quand la quête est complétée (conclusion de la scène) */ + @Column({ name: 'complete_text', type: 'text', nullable: true }) + completeText: string | null; + // Objectif @Column({ name: 'objective_type', length: 30 }) - objectiveType: string; // 'kill_monster' | 'kill_any' | 'gather_material' | 'craft_item' | 'forge_item' + objectiveType: string; // 'kill_monster' | 'kill_any' | 'gather_material' | 'craft_item' | 'forge_item' | 'story_event' @Column({ name: 'objective_target_id', type: 'varchar', length: 255, nullable: true }) objectiveTargetId: string | null; // monster ID or material ID (null for kill_any) diff --git a/src/quest/quest.service.ts b/src/quest/quest.service.ts index 03fd48f..2095cda 100644 --- a/src/quest/quest.service.ts +++ b/src/quest/quest.service.ts @@ -128,11 +128,15 @@ export class QuestService { return this.playerQuestRepo.save(existing); } + // story_event quests complete immediately — they're narrative moments, not grinds + const isStoryEvent = quest.objectiveType === 'story_event'; + const pq = this.playerQuestRepo.create({ characterId, questId, - progress: 0, - status: 'active', + progress: isStoryEvent ? 1 : 0, + status: isStoryEvent ? 'completed' : 'active', + completedAt: isStoryEvent ? new Date() : null, }); return this.playerQuestRepo.save(pq); }