feat: Combat tour par tour — Phases A-D complètes

TurnManager stateless avec sessions en mémoire (TTL 10min).
SpellSystem : 15 sorts (5 par voie du Dao), mana, cooldowns, buffs/debuffs.
CompanionAI : Mira (heal/support) et Vell (tank/dps) — IA contextuelle.
Monster AI : 3 profils (agressif, défensif, chaotique).

Nouvelles entités : Spell, PlayerSpell, PlayerDaoPath.
Character +mana. Monster +aiProfile +isBoss.
Migration : 1743004800000-TurnCombatSystem.

Frontend : TurnCombatPage (select/combat/result), sélecteur compagnon,
barres HP/MP, log scrollable, sous-menu sorts avec cooldowns.

Endpoints : 8 routes sous /combat/turn/ (start, action, session, spells,
unlocked, unlock, dao, dao/choose).

Combat simple (POST /combat/start) et grind ×5/×10 inchangés.
This commit is contained in:
2026-03-25 00:58:47 +01:00
parent 4beb1b2ed9
commit 9d50adf523
21 changed files with 2904 additions and 5 deletions

View File

@@ -0,0 +1,147 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class TurnCombatSystem1743004800000 implements MigrationInterface {
name = 'TurnCombatSystem1743004800000';
public async up(queryRunner: QueryRunner): Promise<void> {
// --- Mana sur characters ---
await queryRunner.query(`
ALTER TABLE \`characters\`
ADD COLUMN \`mana_current\` INT NOT NULL DEFAULT 50 AFTER \`hp_max\`,
ADD COLUMN \`mana_max\` INT NOT NULL DEFAULT 50 AFTER \`mana_current\`
`);
// --- AI profile sur monsters ---
await queryRunner.query(`
ALTER TABLE \`monsters\`
ADD COLUMN \`ai_profile\` VARCHAR(20) NOT NULL DEFAULT 'aggressive' AFTER \`zone\`,
ADD COLUMN \`is_boss\` TINYINT(1) NOT NULL DEFAULT 0 AFTER \`ai_profile\`
`);
// --- Table des sorts ---
await queryRunner.query(`
CREATE TABLE \`spells\` (
\`id\` VARCHAR(36) NOT NULL,
\`name\` VARCHAR(100) NOT NULL,
\`path\` VARCHAR(20) NOT NULL,
\`path_level\` INT NOT NULL,
\`mana_cost\` INT NOT NULL,
\`cooldown\` INT NOT NULL,
\`target_type\` VARCHAR(20) NOT NULL,
\`description\` TEXT NOT NULL,
\`effects\` JSON NOT NULL,
\`unlock_cost\` INT NOT NULL DEFAULT 0,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB
`);
// --- Sorts debloques par joueur ---
await queryRunner.query(`
CREATE TABLE \`player_spells\` (
\`id\` VARCHAR(36) NOT NULL,
\`character_id\` VARCHAR(36) NOT NULL,
\`spell_id\` VARCHAR(36) NOT NULL,
\`unlocked_at\` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`),
UNIQUE INDEX \`IDX_player_spells_char_spell\` (\`character_id\`, \`spell_id\`),
INDEX \`IDX_player_spells_character\` (\`character_id\`),
CONSTRAINT \`FK_player_spells_character\` FOREIGN KEY (\`character_id\`)
REFERENCES \`characters\`(\`id\`) ON DELETE CASCADE,
CONSTRAINT \`FK_player_spells_spell\` FOREIGN KEY (\`spell_id\`)
REFERENCES \`spells\`(\`id\`) ON DELETE CASCADE
) ENGINE=InnoDB
`);
// --- Progression dans les voies du Dao ---
await queryRunner.query(`
CREATE TABLE \`player_dao_paths\` (
\`id\` VARCHAR(36) NOT NULL,
\`character_id\` VARCHAR(36) NOT NULL,
\`path\` VARCHAR(20) NOT NULL,
\`is_primary\` TINYINT(1) NOT NULL DEFAULT 0,
\`path_points\` INT NOT NULL DEFAULT 0,
\`path_level\` INT NOT NULL DEFAULT 0,
PRIMARY KEY (\`id\`),
UNIQUE INDEX \`IDX_player_dao_char_path\` (\`character_id\`, \`path\`),
INDEX \`IDX_player_dao_character\` (\`character_id\`),
CONSTRAINT \`FK_player_dao_character\` FOREIGN KEY (\`character_id\`)
REFERENCES \`characters\`(\`id\`) ON DELETE CASCADE
) ENGINE=InnoDB
`);
// --- Seed des 15 sorts ---
await queryRunner.query(`
INSERT INTO \`spells\` (\`id\`, \`name\`, \`path\`, \`path_level\`, \`mana_cost\`, \`cooldown\`, \`target_type\`, \`description\`, \`effects\`, \`unlock_cost\`) VALUES
-- Ecoute
(UUID(), 'Perception du Flux', 'ecoute', 1, 10, 3, 'enemy',
'Revele les faiblesses de l''ennemi. Buff +20% degats pendant 2 tours.',
'[{"type":"buff","stat":"damage","value":20,"isPercent":true,"duration":2,"log":"{caster} percoit les failles de {target} !"}]', 0),
(UUID(), 'Chant d''Eveil', 'ecoute', 2, 20, 2, 'enemy',
'Degats magiques + debuff Confusion (-30% precision, 2 tours).',
'[{"type":"damage","ratio":2,"ratioStat":"intelligence","log":"{caster} entonne le Chant d''Eveil — {damage} degats !"},{"type":"debuff","stat":"precision","value":30,"isPercent":true,"duration":2,"log":"{target} est Confus !"}]', 3),
(UUID(), 'Ancrage Memoriel', 'ecoute', 3, 15, 4, 'self',
'Annule le prochain debuff ou purifie un debuff actif.',
'[{"type":"purge","stat":"debuff","value":1,"log":"{caster} ancre sa memoire — debuff annule !"}]', 6),
(UUID(), 'Murmure du Courant', 'ecoute', 4, 25, 3, 'enemy',
'Degats magiques (Int x2.5) + drain mana si ennemi caster.',
'[{"type":"damage","ratio":2.5,"ratioStat":"intelligence","log":"{caster} murmure au Courant — {damage} degats !"},{"type":"special","stat":"mana_drain","value":15,"log":"Le Courant aspire l''energie de {target} !"}]', 10),
(UUID(), 'Chant de l''Oubli', 'ecoute', 5, 35, 5, 'enemy',
'Reset cooldowns ennemis + degats (Int x3). Boss : -1 buff au lieu du reset.',
'[{"type":"damage","ratio":3,"ratioStat":"intelligence","log":"{caster} libere le Chant de l''Oubli — {damage} degats !"},{"type":"special","stat":"cooldown_reset","value":0,"log":"Les capacites de {target} sont perturbees !"}]', 15),
-- Resonance
(UUID(), 'Onde de Choc', 'resonance', 1, 15, 2, 'all_enemies',
'Degats physiques AoE (Force x1.5) a tous les ennemis.',
'[{"type":"damage","ratio":1.5,"ratioStat":"force","log":"{caster} declenche une Onde de Choc — {damage} degats !"}]', 0),
(UUID(), 'Bouclier de Flux', 'resonance', 2, 20, 4, 'self',
'Reduit les degats recus de 40% pendant 2 tours.',
'[{"type":"buff","stat":"damage_reduction","value":40,"isPercent":true,"duration":2,"log":"{caster} erige un Bouclier de Flux !"}]', 3),
(UUID(), 'Contre-Courant', 'resonance', 3, 15, 3, 'self',
'Riposte automatique au prochain coup recu (Force x2).',
'[{"type":"buff","stat":"riposte","value":2,"isPercent":false,"duration":1,"log":"{caster} se prepare a la riposte !"}]', 6),
(UUID(), 'Ancre de Pierre', 'resonance', 4, 25, 4, 'self',
'Taunt + boost defense 50% pendant 2 tours.',
'[{"type":"buff","stat":"taunt","value":1,"isPercent":false,"duration":2,"log":"{caster} s''ancre dans la pierre !"},{"type":"buff","stat":"defense","value":50,"isPercent":true,"duration":2,"log":"Defense renforcee !"}]', 10),
(UUID(), 'Fracture Sismique', 'resonance', 5, 40, 5, 'enemy',
'Degats massifs (Force x3.5) + Stun 1 tour.',
'[{"type":"damage","ratio":3.5,"ratioStat":"force","log":"{caster} fracture le sol — {damage} degats !"},{"type":"debuff","stat":"stun","value":1,"isPercent":false,"duration":1,"log":"{target} est etourdi !"}]', 15),
-- Harmonie
(UUID(), 'Chant Apaisant', 'harmonie', 1, 15, 2, 'ally',
'Soin (Int x2 + 10% hpMax).',
'[{"type":"heal","ratio":2,"ratioStat":"intelligence","value":10,"log":"{caster} entonne un chant apaisant — {target} recupere {heal} HP !"}]', 0),
(UUID(), 'Dissolution', 'harmonie', 2, 20, 3, 'enemy',
'Retire tous les buffs d''un ennemi.',
'[{"type":"purge","stat":"buff","value":99,"log":"{caster} dissout les protections de {target} !"}]', 3),
(UUID(), 'Onde de Serenite', 'harmonie', 3, 25, 4, 'all_allies',
'Buff defense +25% + regen 5% hpMax/tour (3 tours) a toute l''equipe.',
'[{"type":"buff","stat":"defense","value":25,"isPercent":true,"duration":3,"log":"Une onde de serenite enveloppe l''equipe !"},{"type":"buff","stat":"regen","value":5,"isPercent":true,"duration":3,"log":"Regeneration active !"}]', 6),
(UUID(), 'Lien du Courant', 'harmonie', 4, 20, 3, 'ally',
'Transfere 30% des degats du joueur au compagnon (ou inverse) pendant 3 tours.',
'[{"type":"buff","stat":"damage_link","value":30,"isPercent":true,"duration":3,"log":"{caster} tisse un lien de Courant avec {target} !"}]', 10),
(UUID(), 'Symphonie Restauratrice', 'harmonie', 5, 45, 6, 'all_allies',
'Full heal equipe + purge tous debuffs + bouclier 1 coup.',
'[{"type":"heal","ratio":0,"ratioStat":"intelligence","value":100,"log":"La Symphonie Restauratrice guerit toute l''equipe !"},{"type":"purge","stat":"debuff","value":99,"log":"Tous les debuffs sont purges !"},{"type":"buff","stat":"shield","value":1,"isPercent":false,"duration":1,"log":"Un bouclier protege chacun du prochain coup !"}]', 15)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS \`player_spells\``);
await queryRunner.query(`DROP TABLE IF EXISTS \`player_dao_paths\``);
await queryRunner.query(`DROP TABLE IF EXISTS \`spells\``);
await queryRunner.query(`ALTER TABLE \`monsters\` DROP COLUMN \`is_boss\`, DROP COLUMN \`ai_profile\``);
await queryRunner.query(`ALTER TABLE \`characters\` DROP COLUMN \`mana_max\`, DROP COLUMN \`mana_current\``);
}
}