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:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-08T09:22:18.219Z
|
Generated: 2026-06-08T09:39:59.023Z
|
||||||
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-08T09:22:16.306Z",
|
"generated": "2026-06-08T09:39:57.039Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── floor-utils.upgraded.test.ts
|
│ │ │ │ ├── floor-utils.upgraded.test.ts
|
||||||
│ │ │ │ ├── formatting.test.ts
|
│ │ │ │ ├── formatting.test.ts
|
||||||
│ │ │ │ ├── guardian-names.test.ts
|
│ │ │ │ ├── guardian-names.test.ts
|
||||||
|
│ │ │ │ ├── hasty-enchanter.test.ts
|
||||||
│ │ │ │ ├── mana-utils.test.ts
|
│ │ │ │ ├── mana-utils.test.ts
|
||||||
│ │ │ │ ├── melee-auto-attack.test.ts
|
│ │ │ │ ├── melee-auto-attack.test.ts
|
||||||
│ │ │ │ ├── melee-defense-bypass.test.ts
|
│ │ │ │ ├── melee-defense-bypass.test.ts
|
||||||
|
|||||||
@@ -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
|
// Progress per tick expressed as a fraction of HOURS_PER_TICK
|
||||||
const DESIGN_PROGRESS_PER_TICK = 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 ──────────────────────────────────────────
|
// ─── Design Creation & Calculation ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -78,17 +78,6 @@ export function calculateDesignTime(effects: DesignEffect[]): number {
|
|||||||
return time;
|
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 ───────────────────────────────────────────────────────
|
// ─── XP & Progression ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number {
|
export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number {
|
||||||
@@ -127,8 +116,10 @@ export function calculateDesignProgress(
|
|||||||
let timeBonus = 0;
|
let timeBonus = 0;
|
||||||
|
|
||||||
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
|
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
|
||||||
timeBonus = DESIGN_PROGRESS_PER_TICK * HASTY_ENCHANTER_BONUS_MULTIPLIER;
|
const baseIncrement = progress - currentProgress;
|
||||||
progress += timeBonus;
|
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) {
|
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < INSTANT_DESIGN_CHANCE) {
|
||||||
|
|||||||
Reference in New Issue
Block a user