Skip to content

Commit 248bd03

Browse files
committed
fix(battle): combats et boutons
1 parent b0e5a39 commit 248bd03

4 files changed

Lines changed: 129 additions & 87 deletions

File tree

worldboss-app/src/events/interactionCreate.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const { Events, MessageFlags } = require('discord.js');
4-
const { handleCombatButton } = require('../services/combat.service');
4+
const { handleCombatButton, handleActionSelect, handleActionBack } = require('../services/combat.service');
55
const { handleDungeonNext, startDungeon, showDungeonSelection } = require('../services/dungeon.service');
66
const { getPendingLoot, deletePendingLoot, deleteDungeonState, deleteCombatState } = require('../cache/redis');
77
const { applyLoot } = require('../engines/lootEngine');
@@ -192,6 +192,8 @@ module.exports = {
192192
} catch (err) {
193193
console.error('[Wiki/select]', err);
194194
}
195+
} else if (interaction.customId === 'combat_action_select') {
196+
return await handleActionSelect(interaction);
195197
} else if (interaction.customId === 'combat_consumable') {
196198
const itemId = interaction.values[0];
197199
interaction.customId = `combat_item_${itemId}`;
@@ -488,6 +490,10 @@ module.exports = {
488490
return interaction.editReply({ embeds: [embed], components: [] });
489491
}
490492

493+
if (customId === 'combat_action_back') {
494+
return await handleActionBack(interaction);
495+
}
496+
491497
if (customId.startsWith('combat_')) {
492498
return await handleCombatButton(interaction);
493499
}

worldboss-app/src/services/combat.service.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { applyLoot } = require('../engines/lootEngine');
77
const { addXp, addGold, computeRegenedHp, rankXpRequired, MAX_LEVEL } = require('./player.service');
88
const { getCharacterEmoji } = require('../data/races');
99
const { computeStats, xpRequired } = require('../utils/stats');
10-
const { buildCombatEmbed, buildCombatRow, buildDungeonNextRow, errorEmbed } = require('../utils/embed');
10+
const { buildCombatEmbed, buildCombatRow, buildTargetRow, buildDungeonNextRow, errorEmbed } = require('../utils/embed');
1111
const { animateCombatLogs, sleep, animateXpGain } = require('../utils/animate');
1212
const { EmbedBuilder, MessageFlags } = require('discord.js');
1313
const { DUNGEONS } = require('../data/dungeons');
@@ -389,4 +389,55 @@ async function handleCombatButton(interaction) {
389389
);
390390
}
391391

392-
module.exports = { buildCombatState, handleCombatButton };
392+
async function handleActionSelect(interaction) {
393+
const userId = interaction.user.id;
394+
const guildId = interaction.guildId;
395+
396+
const character = await prisma.character.findUnique({
397+
where: { userId_guildId: { userId, guildId } },
398+
select: { id: true },
399+
});
400+
if (!character) {
401+
return interaction.reply({ embeds: [errorEmbed('Personnage introuvable.')], flags: MessageFlags.Ephemeral });
402+
}
403+
404+
const state = await getCombatState(character.id);
405+
if (!state || state.status !== 'active') {
406+
return interaction.reply({ embeds: [errorEmbed('Aucun combat en cours.')], flags: MessageFlags.Ephemeral });
407+
}
408+
409+
const action = interaction.values[0]; // 'attack' | 'skill_<key>'
410+
const aliveEnemies = state.enemies.filter((e) => e.hp > 0);
411+
412+
if (aliveEnemies.length <= 1) {
413+
const targetIdx = Math.max(0, state.enemies.findIndex((e) => e.hp > 0));
414+
interaction.customId = `combat_${action}:${targetIdx}`;
415+
return handleCombatButton(interaction);
416+
}
417+
418+
await interaction.deferUpdate();
419+
return interaction.editReply({ embeds: [buildCombatEmbed(state)], components: buildTargetRow(action, state) });
420+
}
421+
422+
async function handleActionBack(interaction) {
423+
const userId = interaction.user.id;
424+
const guildId = interaction.guildId;
425+
426+
const character = await prisma.character.findUnique({
427+
where: { userId_guildId: { userId, guildId } },
428+
select: { id: true },
429+
});
430+
if (!character) {
431+
return interaction.reply({ embeds: [errorEmbed('Personnage introuvable.')], flags: MessageFlags.Ephemeral });
432+
}
433+
434+
const state = await getCombatState(character.id);
435+
if (!state || state.status !== 'active') {
436+
return interaction.reply({ embeds: [errorEmbed('Aucun combat en cours.')], flags: MessageFlags.Ephemeral });
437+
}
438+
439+
await interaction.deferUpdate();
440+
return interaction.editReply({ embeds: [buildCombatEmbed(state)], components: buildCombatRow(state) });
441+
}
442+
443+
module.exports = { buildCombatState, handleCombatButton, handleActionSelect, handleActionBack };

worldboss-app/src/utils/embed.js

Lines changed: 68 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -136,40 +136,55 @@ function buildCombatEmbed(state) {
136136
return embed;
137137
}
138138

139-
/**
140-
* Build combat action rows.
141-
* Single enemy → one row (attack, skill, potion, flee).
142-
* Multiple enemies → attack row per target, then skill row per target, then utils row.
143-
* Returns an array of ActionRowBuilder.
144-
*/
145-
function buildCombatRow(state) {
146-
const { player, enemies } = state;
147-
const skills = player.activeSkills ?? [];
148-
const cooldowns = player.skillCooldowns ?? {};
149-
const usedOnce = player.usedOnceSkills ?? [];
150-
const aliveEnemies = enemies.filter((e) => e.hp > 0);
151-
152-
// Build consumable select menu
153-
const availableConsumables = (player.consumables ?? []).filter((c) => {
139+
function buildConsumableRow(player) {
140+
const available = (player.consumables ?? []).filter((c) => {
154141
const def = ITEMS[c.itemId];
155142
if (!def?.effect) return false;
156-
if (def.effect.type === 'restore_ap') return false; // not applicable in combat
143+
if (def.effect.type === 'restore_ap') return false;
157144
return c.quantity > 0 || c.quantity === -1;
158145
});
146+
if (!available.length) return null;
147+
return new ActionRowBuilder().addComponents(
148+
new StringSelectMenuBuilder()
149+
.setCustomId('combat_consumable')
150+
.setPlaceholder('🎒 Utiliser un consommable…')
151+
.addOptions(available.map((c) => {
152+
const def = ITEMS[c.itemId];
153+
const qty = c.quantity === -1 ? '∞' : ${c.quantity}`;
154+
return { label: `${def.name} (${qty})`, value: c.itemId };
155+
})),
156+
);
157+
}
159158

160-
const consumableRow = availableConsumables.length > 0
161-
? new ActionRowBuilder().addComponents(
162-
new StringSelectMenuBuilder()
163-
.setCustomId('combat_consumable')
164-
.setPlaceholder('🎒 Utiliser un consommable…')
165-
.addOptions(availableConsumables.map((c) => {
166-
const def = ITEMS[c.itemId];
167-
const qty = c.quantity === -1 ? '∞' : ${c.quantity}`;
168-
return { label: `${def.name} (${qty})`, value: c.itemId };
169-
})),
170-
)
171-
: null;
159+
/**
160+
* Phase 1 — action select + consumable select + flee button (always ≤ 3 rows).
161+
*/
162+
function buildCombatRow(state) {
163+
const { player } = state;
164+
const skills = player.activeSkills ?? [];
165+
const cooldowns = player.skillCooldowns ?? {};
166+
const usedOnce = player.usedOnceSkills ?? [];
167+
168+
const actionRow = new ActionRowBuilder().addComponents(
169+
new StringSelectMenuBuilder()
170+
.setCustomId('combat_action_select')
171+
.setPlaceholder('⚔️ Choisir une action…')
172+
.addOptions([
173+
{ label: 'Attaquer', value: 'attack', emoji: '⚔️' },
174+
...skills.map((sk) => {
175+
const cd = cooldowns[sk.key] ?? 0;
176+
const blocked = cd > 0 || (sk.oncePerCombat && usedOnce.includes(sk.key));
177+
return {
178+
label: blocked ? `${sk.name} (${cd}t)` : sk.name,
179+
value: `skill_${sk.key}`,
180+
emoji: '🔥',
181+
description: blocked ? 'Non disponible ce tour' : undefined,
182+
};
183+
}),
184+
]),
185+
);
172186

187+
const consumableRow = buildConsumableRow(player);
173188
const fleeRow = new ActionRowBuilder().addComponents(
174189
new ButtonBuilder()
175190
.setCustomId('combat_flee')
@@ -178,70 +193,39 @@ function buildCombatRow(state) {
178193
.setStyle(ButtonStyle.Secondary),
179194
);
180195

181-
const targetIdx = enemies.findIndex((e) => e.hp > 0);
182-
const targetIdx0 = targetIdx >= 0 ? targetIdx : 0;
196+
const rows = [actionRow];
197+
if (consumableRow) rows.push(consumableRow);
198+
rows.push(fleeRow);
199+
return rows;
200+
}
183201

184-
if (aliveEnemies.length <= 1) {
185-
const attackRow = new ActionRowBuilder().addComponents(
186-
new ButtonBuilder()
187-
.setCustomId(`combat_attack:${targetIdx0}`)
188-
.setLabel('Attaquer')
189-
.setEmoji('⚔️')
190-
.setStyle(ButtonStyle.Danger),
191-
...skills.map((sk) => {
192-
const cd = cooldowns[sk.key] ?? 0;
193-
const blocked = cd > 0 || (sk.oncePerCombat && usedOnce.includes(sk.key));
194-
const label = cd > 0 ? `${sk.name} (${cd}t)` : sk.name;
195-
return new ButtonBuilder()
196-
.setCustomId(`combat_skill_${sk.key}:${targetIdx0}`)
197-
.setLabel(label)
198-
.setEmoji('🔥')
199-
.setStyle(ButtonStyle.Primary)
200-
.setDisabled(blocked);
201-
}),
202-
new ButtonBuilder()
203-
.setCustomId('combat_flee')
204-
.setLabel('Fuir')
205-
.setEmoji('🏃')
206-
.setStyle(ButtonStyle.Secondary),
207-
);
208-
const rows = [attackRow];
209-
if (consumableRow) rows.push(consumableRow);
210-
return rows;
211-
}
202+
/**
203+
* Phase 2 (multi-ennemis) — boutons de cible + consumable + retour (toujours ≤ 3 rows).
204+
* action : 'attack' | 'skill_<key>'
205+
*/
206+
function buildTargetRow(action, state) {
207+
const { player, enemies } = state;
208+
const aliveEnemies = enemies.filter((e) => e.hp > 0);
212209

213-
// Multiple enemies — attack row, one skill row per skill, consumable select, flee
214-
const attackRow = new ActionRowBuilder().addComponents(
210+
const targetRow = new ActionRowBuilder().addComponents(
215211
...aliveEnemies.map((e) => {
216212
const idx = enemies.indexOf(e);
217213
return new ButtonBuilder()
218-
.setCustomId(`combat_attack:${idx}`)
219-
.setLabel(`⚔️ ${e.name}`)
220-
.setStyle(ButtonStyle.Danger);
214+
.setCustomId(`combat_${action}:${idx}`)
215+
.setLabel(e.name)
216+
.setEmoji('🎯')
217+
.setStyle(ButtonStyle.Primary);
221218
}),
219+
new ButtonBuilder()
220+
.setCustomId('combat_action_back')
221+
.setLabel('Retour')
222+
.setEmoji('↩️')
223+
.setStyle(ButtonStyle.Secondary),
222224
);
223225

224-
const rows = [attackRow];
225-
226-
for (const sk of skills) {
227-
const cd = cooldowns[sk.key] ?? 0;
228-
const blocked = cd > 0 || (sk.oncePerCombat && usedOnce.includes(sk.key));
229-
const label = cd > 0 ? `${sk.name} (${cd}t)` : sk.name;
230-
const skillRow = new ActionRowBuilder().addComponents(
231-
...aliveEnemies.map((e) => {
232-
const idx = enemies.indexOf(e);
233-
return new ButtonBuilder()
234-
.setCustomId(`combat_skill_${sk.key}:${idx}`)
235-
.setLabel(`🔥 ${label}${e.name}`)
236-
.setStyle(ButtonStyle.Primary)
237-
.setDisabled(blocked);
238-
}),
239-
);
240-
rows.push(skillRow);
241-
}
242-
226+
const consumableRow = buildConsumableRow(player);
227+
const rows = [targetRow];
243228
if (consumableRow) rows.push(consumableRow);
244-
rows.push(fleeRow);
245229
return rows;
246230
}
247231

@@ -639,6 +623,7 @@ module.exports = {
639623
hpBarAnsi,
640624
buildCombatEmbed,
641625
buildCombatRow,
626+
buildTargetRow,
642627
buildDungeonNextRow,
643628
buildLootEmbed,
644629
buildProfileEmbed,

worldboss-app/src/utils/stats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function computeStats(user, loadout) {
8989
for (const slotId of allSlots) {
9090
const item = ITEMS[slotId];
9191
if (!item) continue;
92-
if (item.skill) {
92+
if (item.skill && activeSkills.length < 2) {
9393
const sk = SKILLS[item.skill];
9494
if (sk && !activeSkills.find((s) => s.key === item.skill)) {
9595
activeSkills.push({ key: item.skill, ...sk });

0 commit comments

Comments
 (0)