fix: Hasty Enchanter now applies correct 25% design speed bonus (was 6.25%)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// ─── Regression Test: Hasty Enchanter 25% Design Speed Bonus ──────────────────
|
||||
// Bug #301: Hasty Enchanter applied ~6.25% speed increase (additive 0.01 per tick)
|
||||
// instead of the spec-defined 25% (time *= 0.75, i.e. 4/3 progress per tick).
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateDesignProgress } from '../crafting-design';
|
||||
import { HOURS_PER_TICK } from '../constants';
|
||||
|
||||
function makeComputedEffects(specials: string[] = []) {
|
||||
return {
|
||||
maxManaMultiplier: 1,
|
||||
maxManaBonus: 0,
|
||||
regenMultiplier: 1,
|
||||
regenBonus: 0,
|
||||
clickManaMultiplier: 1,
|
||||
clickManaBonus: 0,
|
||||
meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1,
|
||||
conversionEfficiency: 0,
|
||||
baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0,
|
||||
attackSpeedMultiplier: 1,
|
||||
critChanceBonus: 0,
|
||||
critDamageMultiplier: 1,
|
||||
elementalDamageMultiplier: 1,
|
||||
studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1,
|
||||
progressRetention: 0,
|
||||
instantStudyChance: 0,
|
||||
freeStudyChance: 0,
|
||||
elementCapMultiplier: 1,
|
||||
elementCapBonus: 0,
|
||||
perElementCapBonus: {},
|
||||
perElementRegenBonus: {},
|
||||
conversionCostMultiplier: 1,
|
||||
doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set(specials),
|
||||
activeUpgrades: [],
|
||||
skillLevelMultiplier: 1,
|
||||
enchantmentPowerMultiplier: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Hasty Enchanter — design speed bonus', () => {
|
||||
it('should apply 4/3 progress per tick (not additive 0.01) for repeat designs', () => {
|
||||
const effects = makeComputedEffects(['hastyEnchanter']);
|
||||
const required = 2; // 2 hours required
|
||||
const result = calculateDesignProgress(0, required, effects, true);
|
||||
|
||||
// Expected: progress = HOURS_PER_TICK * (4/3) = 0.04 * 1.333... = 0.05333...
|
||||
const expectedProgress = HOURS_PER_TICK * (4 / 3);
|
||||
expect(result.progress).toBeCloseTo(expectedProgress, 10);
|
||||
expect(result.isComplete).toBe(false);
|
||||
});
|
||||
|
||||
it('should reduce total ticks to 75% of original (25% faster)', () => {
|
||||
const effects = makeComputedEffects(['hastyEnchanter']);
|
||||
const required = 2; // 2 hours required
|
||||
|
||||
// Without haste: ticks needed = 2 / 0.04 = 50 ticks
|
||||
// With haste (4/3 multiplier): effective progress per tick = 0.04 * 4/3 = 0.05333...
|
||||
// Ticks needed with haste = 2 / 0.05333... = 37.5 ticks
|
||||
// Ratio = 37.5 / 50 = 0.75
|
||||
|
||||
let progress = 0;
|
||||
let ticks = 0;
|
||||
while (progress < required) {
|
||||
const result = calculateDesignProgress(progress, required, effects, true);
|
||||
progress = result.progress;
|
||||
ticks++;
|
||||
if (ticks > 1000) break; // safety valve
|
||||
}
|
||||
|
||||
// 50 ticks * 0.75 = 37.5 ticks expected
|
||||
expect(ticks).toBeLessThanOrEqual(38);
|
||||
expect(ticks).toBeGreaterThanOrEqual(37);
|
||||
});
|
||||
|
||||
it('should NOT apply haste bonus for first-time designs (isRepeatDesign=false)', () => {
|
||||
const effects = makeComputedEffects(['hastyEnchanter']);
|
||||
const required = 2;
|
||||
const result = calculateDesignProgress(0, required, effects, false);
|
||||
|
||||
// Without haste: progress = HOURS_PER_TICK = 0.04
|
||||
expect(result.progress).toBeCloseTo(HOURS_PER_TICK, 10);
|
||||
expect(result.timeBonus).toBe(0);
|
||||
});
|
||||
|
||||
it('should NOT apply haste bonus without HASTY_ENCHANTER special', () => {
|
||||
const effects = makeComputedEffects([]); // no specials
|
||||
const required = 2;
|
||||
const result = calculateDesignProgress(0, required, effects, true);
|
||||
|
||||
expect(result.progress).toBeCloseTo(HOURS_PER_TICK, 10);
|
||||
expect(result.timeBonus).toBe(0);
|
||||
});
|
||||
|
||||
it('timeBonus should reflect the extra progress beyond base tick', () => {
|
||||
const effects = makeComputedEffects(['hastyEnchanter']);
|
||||
const required = 2;
|
||||
const result = calculateDesignProgress(0, required, effects, true);
|
||||
|
||||
// timeBonus = hastedIncrement - baseIncrement = (0.04 * 4/3) - 0.04 = 0.04 * (1/3) ≈ 0.01333...
|
||||
const expectedBonus = HOURS_PER_TICK * (1 / 3);
|
||||
expect(result.timeBonus).toBeCloseTo(expectedBonus, 10);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { EQUIPMENT_TYPES } from './data/equipment';
|
||||
|
||||
// Progress per tick expressed as a fraction of HOURS_PER_TICK
|
||||
const DESIGN_PROGRESS_PER_TICK = HOURS_PER_TICK;
|
||||
const HASTY_ENCHANTER_BONUS_MULTIPLIER = 0.25;
|
||||
const HASTY_ENCHANTER_SPEED_MULTIPLIER = 4 / 3; // 1 / 0.75 — 25% less time means 4/3 progress per tick
|
||||
|
||||
// ─── Design Creation & Calculation ──────────────────────────────────────────
|
||||
|
||||
@@ -78,17 +78,6 @@ export function calculateDesignTime(effects: DesignEffect[]): number {
|
||||
return time;
|
||||
}
|
||||
|
||||
export function getDesignTimeWithHaste(
|
||||
effects: DesignEffect[],
|
||||
isRepeatDesign: boolean,
|
||||
computedEffects: ComputedEffects
|
||||
): number {
|
||||
const time = calculateDesignTime(effects);
|
||||
return isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)
|
||||
? time * 0.75
|
||||
: time;
|
||||
}
|
||||
|
||||
// ─── XP & Progression ───────────────────────────────────────────────────────
|
||||
|
||||
export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number {
|
||||
@@ -127,8 +116,10 @@ export function calculateDesignProgress(
|
||||
let timeBonus = 0;
|
||||
|
||||
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
|
||||
timeBonus = DESIGN_PROGRESS_PER_TICK * HASTY_ENCHANTER_BONUS_MULTIPLIER;
|
||||
progress += timeBonus;
|
||||
const baseIncrement = progress - currentProgress;
|
||||
const hastedIncrement = baseIncrement * HASTY_ENCHANTER_SPEED_MULTIPLIER;
|
||||
progress = currentProgress + hastedIncrement;
|
||||
timeBonus = hastedIncrement - baseIncrement;
|
||||
}
|
||||
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < INSTANT_DESIGN_CHANCE) {
|
||||
|
||||
Reference in New Issue
Block a user