fix: endurance regen 6min→3min dans combat/forge/craft + potions d'énergie
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 32s

Bug: combat/forge/craft calculaient la regen à 1pt/6min (ancien) alors que
character.service utilisait 1pt/3min (nouveau). Le joueur voyait 8 endurance
dans le HUD mais le backend refusait le combat avec 4.

Potions d'énergie: Potion (30 endurance, 20 or) + Grande (60 endurance, 45 or).
Consommable instantané via la boutique — le joueur peut acheter du temps de jeu.
This commit is contained in:
2026-03-24 17:51:30 +01:00
parent 1ffde61f97
commit 8cb5fcd5ba
6 changed files with 49 additions and 10 deletions

View File

@@ -1,7 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { characterApi } from '../api/endpoints'; import { characterApi } from '../api/endpoints';
import { api } from '../api/client'; import { api } from '../api/client';
import { Coins, ShoppingBag, Sword, Shield, Heart } from 'lucide-react'; import { Coins, ShoppingBag, Sword, Shield, Heart, Zap } from 'lucide-react';
const RARITY_COLORS: Record<string, string> = { const RARITY_COLORS: Record<string, string> = {
common: '#9ca3af', common: '#9ca3af',
@@ -58,7 +58,10 @@ function ShopItemCard({ item, onBuy, buying }: { item: ShopItem; onBuy: () => vo
<div style={{ display: 'flex', gap: 12, fontSize: 11, color: '#6b7a99' }}> <div style={{ display: 'flex', gap: 12, fontSize: 11, color: '#6b7a99' }}>
{item.attackBonus > 0 && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Sword size={10} color="#f4c94e" /> +{item.attackBonus} ATK</span>} {item.attackBonus > 0 && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Sword size={10} color="#f4c94e" /> +{item.attackBonus} ATK</span>}
{item.defenseBonus > 0 && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Shield size={10} color="#5ba4f5" /> +{item.defenseBonus} DEF</span>} {item.defenseBonus > 0 && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Shield size={10} color="#5ba4f5" /> +{item.defenseBonus} DEF</span>}
{item.type === 'consumable' && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Heart size={10} color="#e84040" /> +50% PV</span>} {item.type === 'consumable' && (item as any).forceBonus > 0
? <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Zap size={10} color="#5ba4f5" /> +{(item as any).forceBonus} endurance</span>
: item.type === 'consumable' && <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}><Heart size={10} color="#e84040" /> +50% PV</span>
}
{item.minLevel > 1 && <span>Niv. {item.minLevel}+</span>} {item.minLevel > 1 && <span>Niv. {item.minLevel}+</span>}
</div> </div>
</div> </div>
@@ -131,8 +134,10 @@ export function ShopPage() {
{buyMut.isSuccess && ( {buyMut.isSuccess && (
<div className="card card-gold" style={{ marginBottom: '1rem', padding: '0.5rem 1rem', fontSize: 13, textAlign: 'center' }}> <div className="card card-gold" style={{ marginBottom: '1rem', padding: '0.5rem 1rem', fontSize: 13, textAlign: 'center' }}>
{(buyMut.data as any)?.type === 'consumable' {(buyMut.data as any)?.effectType === 'endurance'
? `🧪 ${(buyMut.data as any)?.item} utilisé ! +${(buyMut.data as any)?.effect?.healed} PV` ? ` ${(buyMut.data as any)?.item} +${(buyMut.data as any)?.effect?.restored} endurance`
: (buyMut.data as any)?.effectType === 'hp'
? `🧪 ${(buyMut.data as any)?.item} — +${(buyMut.data as any)?.effect?.healed} PV`
: `${(buyMut.data as any)?.item} acheté ! (-${(buyMut.data as any)?.goldSpent} or)` : `${(buyMut.data as any)?.item} acheté ! (-${(buyMut.data as any)?.goldSpent} or)`
} }
</div> </div>

View File

@@ -57,7 +57,7 @@ export class CombatService {
// Calculer l'endurance actuelle (lazy pattern) // Calculer l'endurance actuelle (lazy pattern)
const elapsedMinutes = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000; const elapsedMinutes = (Date.now() - character.lastEnduranceTs.getTime()) / 60_000;
const recharge = Math.floor(elapsedMinutes / 6); const recharge = Math.floor(elapsedMinutes / 3);
const enduranceCurrent = Math.min(character.enduranceSaved + recharge, character.enduranceMax); const enduranceCurrent = Math.min(character.enduranceSaved + recharge, character.enduranceMax);
if (enduranceCurrent < COMBAT_ENDURANCE_COST) { if (enduranceCurrent < COMBAT_ENDURANCE_COST) {

View File

@@ -40,7 +40,7 @@ export class CraftService {
// Calculer endurance actuelle (lazy pattern) // Calculer endurance actuelle (lazy pattern)
const elapsedMinutes = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000; const elapsedMinutes = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000;
const recharge = Math.floor(elapsedMinutes / 6); const recharge = Math.floor(elapsedMinutes / 3);
const enduranceCurrent = Math.min(char.enduranceSaved + recharge, char.enduranceMax); const enduranceCurrent = Math.min(char.enduranceSaved + recharge, char.enduranceMax);
if (enduranceCurrent < recipe.enduranceCost) { if (enduranceCurrent < recipe.enduranceCost) {

View File

@@ -47,8 +47,11 @@ const ITEMS = [
{ name: 'Armure du Pharaon', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 18, buyPrice: 1300, minLevel: 13, zone: 'desert', description: 'Dorée et ancienne, elle irradie de puissance.' }, { name: 'Armure du Pharaon', type: 'armor', rarity: 'epic', attackBonus: 0, defenseBonus: 18, buyPrice: 1300, minLevel: 13, zone: 'desert', description: 'Dorée et ancienne, elle irradie de puissance.' },
// Potions — consommables // Potions — consommables
{ name: 'Potion de soin', type: 'consumable', rarity: 'common', attackBonus: 0, defenseBonus: 0, buyPrice: 15, minLevel: 1, zone: null, description: 'Restaure 50% des PV.' }, // HP potions: forceBonus = 0 → heal 50% HP
{ name: 'Grande potion de soin', type: 'consumable', rarity: 'rare', attackBonus: 0, defenseBonus: 0, buyPrice: 40, minLevel: 5, zone: null, description: 'Restaure 50% des PV. (même effet, plus cher — placeholder pour futur)' }, { name: 'Potion de soin', type: 'consumable', rarity: 'common', attackBonus: 0, defenseBonus: 0, forceBonus: 0, buyPrice: 15, minLevel: 1, zone: null, description: 'Restaure 50% des PV.' },
// Endurance potions: forceBonus > 0 → restore N endurance
{ name: 'Potion d\'énergie', type: 'consumable', rarity: 'common', attackBonus: 0, defenseBonus: 0, forceBonus: 30, buyPrice: 20, minLevel: 1, zone: null, description: 'Restaure 30 points d\'endurance.' },
{ name: 'Grande potion d\'énergie', type: 'consumable', rarity: 'rare', attackBonus: 0, defenseBonus: 0, forceBonus: 60, buyPrice: 45, minLevel: 3, zone: null, description: 'Restaure 60 points d\'endurance.' },
]; ];
export async function seedZones(dataSource: DataSource) { export async function seedZones(dataSource: DataSource) {

View File

@@ -70,7 +70,7 @@ export class ForgeService {
// Vérifier endurance // Vérifier endurance
const elapsedMinutes = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000; const elapsedMinutes = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000;
const recharge = Math.floor(elapsedMinutes / 6); const recharge = Math.floor(elapsedMinutes / 3);
const enduranceCurrent = Math.min(char.enduranceSaved + recharge, char.enduranceMax); const enduranceCurrent = Math.min(char.enduranceSaved + recharge, char.enduranceMax);
if (enduranceCurrent < FORGE_ENDURANCE_COST) { if (enduranceCurrent < FORGE_ENDURANCE_COST) {

View File

@@ -62,12 +62,42 @@ export class ShopService {
// Consumable = effet immédiat, pas d'inventaire // Consumable = effet immédiat, pas d'inventaire
if (item.type === 'consumable') { if (item.type === 'consumable') {
char.gold -= item.buyPrice; // Endurance potion: forceBonus > 0 = endurance restore amount
const isEndurancePotion = item.forceBonus > 0;
if (isEndurancePotion) {
// Calc current endurance (lazy)
const elapsed = (Date.now() - char.lastEnduranceTs.getTime()) / 60_000;
const recharge = Math.floor(elapsed / 3);
const currentEndurance = Math.min(char.enduranceSaved + recharge, char.enduranceMax);
if (currentEndurance >= char.enduranceMax) {
throw new BadRequestException('Endurance déjà au maximum');
}
char.gold -= item.buyPrice;
const endBefore = currentEndurance;
const restored = Math.min(item.forceBonus, char.enduranceMax - currentEndurance);
char.enduranceSaved = currentEndurance + restored;
char.lastEnduranceTs = new Date();
await manager.save(char);
return {
bought: true,
item: item.name,
type: 'consumable',
effectType: 'endurance',
goldSpent: item.buyPrice,
effect: { enduranceBefore: endBefore, enduranceAfter: char.enduranceSaved, restored },
};
}
// HP potion
if (char.hpCurrent >= char.hpMax) { if (char.hpCurrent >= char.hpMax) {
throw new BadRequestException('PV déjà au maximum'); throw new BadRequestException('PV déjà au maximum');
} }
char.gold -= item.buyPrice;
const hpBefore = char.hpCurrent; const hpBefore = char.hpCurrent;
char.hpCurrent = Math.min(char.hpMax, char.hpCurrent + Math.floor(char.hpMax * POTION_HEAL_RATIO)); char.hpCurrent = Math.min(char.hpMax, char.hpCurrent + Math.floor(char.hpMax * POTION_HEAL_RATIO));
await manager.save(char); await manager.save(char);
@@ -76,6 +106,7 @@ export class ShopService {
bought: true, bought: true,
item: item.name, item: item.name,
type: 'consumable', type: 'consumable',
effectType: 'hp',
goldSpent: item.buyPrice, goldSpent: item.buyPrice,
effect: { hpBefore, hpAfter: char.hpCurrent, healed: char.hpCurrent - hpBefore }, effect: { hpBefore, hpAfter: char.hpCurrent, healed: char.hpCurrent - hpBefore },
}; };