fix: improve Discipline tab UX - remove confusing base cost label, convert drain to /sec, show computed perk effects and perk-augmented stat totals, fix /tick label suffixes in data
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-28T10:41:19.223Z
|
Generated: 2026-05-28T11:15:20.183Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-28T10:41:17.557Z",
|
"generated": "2026-05-28T11:15:18.333Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── SpireSummaryTab.test.ts
|
│ │ │ │ ├── SpireSummaryTab.test.ts
|
||||||
│ │ │ │ ├── SpireSummaryTab.tsx
|
│ │ │ │ ├── SpireSummaryTab.tsx
|
||||||
│ │ │ │ ├── StatsTab.tsx
|
│ │ │ │ ├── StatsTab.tsx
|
||||||
|
│ │ │ │ ├── disciplines-utils.ts
|
||||||
│ │ │ │ ├── guardian-pacts-components.tsx
|
│ │ │ │ ├── guardian-pacts-components.tsx
|
||||||
│ │ │ │ └── index.ts
|
│ │ │ │ └── index.ts
|
||||||
│ │ │ ├── ActionButtons.tsx
|
│ │ │ ├── ActionButtons.tsx
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
||||||
import type { ManaType } from '@/lib/game/types/elements';
|
import type { ManaType } from '@/lib/game/types/elements';
|
||||||
@@ -16,6 +16,8 @@ import { useManaStore } from '@/lib/game/stores/manaStore';
|
|||||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
|
import { TICKS_PER_SECOND, computePerkCurrentEffect, computeTotalPerkBonusForStat } from './disciplines-utils';
|
||||||
|
import type { ComputedPerkEffect } from './disciplines-utils';
|
||||||
|
|
||||||
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -33,91 +35,63 @@ const ATTUNEMENT_TABS: AttunementTab[] = [
|
|||||||
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Discipline Card Props (split from monolithic 15-field interface) ────────
|
// ─── Discipline Card Props ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface DisciplineCardDefinition {
|
interface DisciplineCardProps {
|
||||||
id: string;
|
definition: DisciplineDefinition;
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
manaType: ManaType;
|
|
||||||
baseCost: number;
|
|
||||||
perkThresholds?: number[];
|
|
||||||
perkValues?: number[];
|
|
||||||
perkTypes?: string[];
|
|
||||||
perkDescriptions?: string[];
|
|
||||||
statBonus: string;
|
|
||||||
statBonusLabel: string;
|
|
||||||
requires?: string[];
|
|
||||||
baseValue: number;
|
|
||||||
drainBase: number;
|
|
||||||
difficultyFactor: number;
|
|
||||||
scalingFactor: number;
|
|
||||||
sourceManaTypes?: ManaType[];
|
|
||||||
conversionRate?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DisciplineCardRuntime {
|
|
||||||
xp: number;
|
xp: number;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
missingPrereqs: string[];
|
missingPrereqs: string[];
|
||||||
missingSourceMana: string[];
|
missingSourceMana: string[];
|
||||||
}
|
|
||||||
|
|
||||||
export interface DisciplineCardCallbacks {
|
|
||||||
onToggle: (id: string, paused: boolean) => void;
|
onToggle: (id: string, paused: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisciplineCardProps {
|
|
||||||
definition: DisciplineCardDefinition;
|
|
||||||
runtime: DisciplineCardRuntime;
|
|
||||||
callbacks: DisciplineCardCallbacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Discipline Card Component ───────────────────────────────────────────────
|
// ─── Discipline Card Component ───────────────────────────────────────────────
|
||||||
|
|
||||||
const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, callbacks }) => {
|
const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
||||||
|
definition, xp, paused: isPaused, concurrentLimit,
|
||||||
|
isLocked, missingPrereqs, missingSourceMana, onToggle,
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes, perkDescriptions,
|
id, name, description, manaType, perks,
|
||||||
statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor,
|
statBonus, baseValue, drainBase, difficultyFactor, scalingFactor,
|
||||||
|
conversionRate, sourceManaTypes,
|
||||||
} = definition;
|
} = definition;
|
||||||
const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs, missingSourceMana } = runtime;
|
|
||||||
const { onToggle } = callbacks;
|
|
||||||
|
|
||||||
const displayXp = xp;
|
const displayXp = xp;
|
||||||
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
|
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
|
||||||
|
|
||||||
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
|
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
|
||||||
const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor);
|
const drainPerSecond = calculateManaDrain(drainBase, displayXp, difficultyFactor) * TICKS_PER_SECOND;
|
||||||
|
|
||||||
const elementDef = ELEMENTS[manaType];
|
const elementDef = ELEMENTS[manaType];
|
||||||
const manaColor = elementDef?.color ?? '#888888';
|
const manaColor = elementDef?.color ?? '#888888';
|
||||||
const manaIcon = elementDef?.sym ?? '✦';
|
const manaIcon = elementDef?.sym ?? '✦';
|
||||||
const manaName = elementDef?.name ?? manaType;
|
const manaName = elementDef?.name ?? manaType;
|
||||||
|
|
||||||
const effectiveIsLocked = isLocked || missingSourceMana.length > 0;
|
const effectiveIsLocked = isLocked || missingSourceMana.length > 0;
|
||||||
|
|
||||||
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
const statBonusLabel = statBonus.label;
|
||||||
const threshold = perkThresholds?.[idx];
|
|
||||||
if (threshold === undefined) return acc;
|
|
||||||
const desc = perkDescriptions?.[idx];
|
|
||||||
if (typ === 'once' || typ === 'infinite') {
|
|
||||||
if (displayXp >= threshold && desc) acc.push(desc);
|
|
||||||
} else if (typ === 'capped') {
|
|
||||||
const interval = perkValues?.[idx] ?? 1;
|
|
||||||
const tier = Math.max(0, Math.floor((displayXp - threshold) / interval) + 1);
|
|
||||||
if (tier > 0 && desc) acc.push(desc);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleAction = () => {
|
// Compute perk effects with current values
|
||||||
onToggle(id, isPaused);
|
const computedPerks = useMemo((): ComputedPerkEffect[] => {
|
||||||
};
|
if (!perks) return [];
|
||||||
|
return perks.map((perk) => ({
|
||||||
|
description: perk.description,
|
||||||
|
currentEffect: computePerkCurrentEffect(perk, displayXp),
|
||||||
|
}));
|
||||||
|
}, [perks, displayXp]);
|
||||||
|
|
||||||
|
// Perk-augmented total for the stat bonus
|
||||||
|
const perkBonusTotal = useMemo(() => {
|
||||||
|
if (!perks || perks.length === 0) return 0;
|
||||||
|
return computeTotalPerkBonusForStat(perks, displayXp, statBonus.stat);
|
||||||
|
}, [perks, displayXp, statBonus.stat]);
|
||||||
|
|
||||||
|
const statBonusTotal = activeStatBonus + perkBonusTotal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={id} className={clsx('border rounded-lg p-4 shadow-sm space-y-3', effectiveIsLocked && 'opacity-60 border-gray-600')}>
|
<div className={clsx('border rounded-lg p-4 shadow-sm space-y-3', effectiveIsLocked && 'opacity-60 border-gray-600')}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-lg font-medium">{name}</h3>
|
<h3 className="text-lg font-medium">{name}</h3>
|
||||||
<span
|
<span
|
||||||
@@ -135,6 +109,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400">{description}</p>
|
<p className="text-sm text-gray-400">{description}</p>
|
||||||
|
|
||||||
|
{/* XP Progress */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
||||||
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
||||||
@@ -145,54 +120,63 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Row */}
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-400">
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-400">
|
||||||
<span>
|
<span><strong>Drain:</strong> {drainPerSecond.toFixed(1)}/sec</span>
|
||||||
<strong>Drain:</strong> {estimatedDrain.toFixed(1)}/tick
|
<span><strong>XP:</strong> {displayXp}</span>
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>Base Cost:</strong> {baseCost}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>XP:</strong> {displayXp}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{definition.conversionRate != null && definition.sourceManaTypes && (
|
{/* Conversion Info */}
|
||||||
|
{conversionRate != null && sourceManaTypes && (
|
||||||
<div className="mt-2 text-xs text-gray-400">
|
<div className="mt-2 text-xs text-gray-400">
|
||||||
<strong>Converts:</strong> {definition.sourceManaTypes.map(s => s === 'raw' ? 'raw' : ELEMENTS[s]?.name ?? s).join(' + ')} → {manaName}
|
<strong>Converts:</strong> {sourceManaTypes.map(s => s === 'raw' ? 'raw' : ELEMENTS[s]?.name ?? s).join(' + ')} → {manaName}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Stat Bonus with Perk Total */}
|
||||||
<div className="mt-2 text-sm">
|
<div className="mt-2 text-sm">
|
||||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
|
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)}/sec on {statBonusLabel}
|
||||||
|
{perkBonusTotal > 0 && (
|
||||||
|
<span className="text-green-400 ml-1">
|
||||||
|
({statBonusTotal.toFixed(2)}/sec with perks)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Perks */}
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<strong>Perks:</strong>
|
<strong>Perks:</strong>
|
||||||
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
||||||
{unlockedPerks && unlockedPerks.length > 0 ? (
|
{computedPerks.length > 0 ? (
|
||||||
unlockedPerks.map((p) => (
|
computedPerks.map((p) => (
|
||||||
<li key={p} className="text-green-500">{p}</li>
|
<li key={p.description} className={clsx(
|
||||||
|
p.currentEffect.startsWith('at ') ? 'text-gray-400' : 'text-green-500',
|
||||||
|
)}>
|
||||||
|
{p.description}
|
||||||
|
<span className="text-gray-300 ml-1">— {p.currentEffect}</span>
|
||||||
|
</li>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<li className="text-gray-400">—locked—</li>
|
<li className="text-gray-400">— none —</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Lock Reasons */}
|
||||||
{effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
|
{effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
|
||||||
<div className="mt-2 text-xs text-red-400">
|
<div className="mt-2 text-xs text-red-400">
|
||||||
<strong>Requires:</strong> {[...missingPrereqs, ...missingSourceMana].join(', ')}
|
<strong>Requires:</strong> {[...missingPrereqs, ...missingSourceMana].join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={toggleAction}
|
onClick={() => onToggle(id, isPaused)}
|
||||||
disabled={effectiveIsLocked}
|
disabled={effectiveIsLocked}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded px-3 py-1 text-sm font-medium',
|
'rounded px-3 py-1 text-sm font-medium',
|
||||||
isLocked
|
effectiveIsLocked
|
||||||
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||||
: isPaused
|
: isPaused
|
||||||
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
||||||
@@ -236,24 +220,21 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{ATTUNEMENT_TABS.map((tab) => {
|
{ATTUNEMENT_TABS.map((tab) => (
|
||||||
const isActiveTab = activeAttunement === tab.key;
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
onClick={() => setActiveAttunement(tab.key)}
|
onClick={() => setActiveAttunement(tab.key)}
|
||||||
className={clsx('rounded px-3 py-1', {
|
className={clsx('rounded px-3 py-1', {
|
||||||
'bg-blue-600 text-white': isActiveTab,
|
'bg-blue-600 text-white': activeAttunement === tab.key,
|
||||||
'text-gray-600': !isActiveTab,
|
'text-gray-600': activeAttunement !== tab.key,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Discipline cards — only render active tab */}
|
{/* Discipline cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{activeTab?.items.map((disc) => {
|
{activeTab?.items.map((disc) => {
|
||||||
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
||||||
@@ -261,47 +242,24 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<DisciplineCard
|
<DisciplineCard
|
||||||
key={disc.id}
|
key={disc.id}
|
||||||
definition={{
|
definition={disc}
|
||||||
id: disc.id,
|
xp={discState.xp}
|
||||||
name: disc.name,
|
paused={discState.paused}
|
||||||
description: disc.description,
|
concurrentLimit={concurrentLimit}
|
||||||
perkThresholds: disc.perks?.map((p) => p.threshold),
|
isLocked={!prereqCheck.canProceed}
|
||||||
perkValues: disc.perks?.map((p) => p.value),
|
missingPrereqs={prereqCheck.missingPrereqs}
|
||||||
perkTypes: disc.perks?.map((p) => p.type),
|
missingSourceMana={disc.sourceManaTypes
|
||||||
perkDescriptions: disc.perks?.map((p) => p.description),
|
? disc.sourceManaTypes
|
||||||
manaType: disc.manaType,
|
.filter((src) => src !== 'raw' && (!elements[src] || !elements[src].unlocked))
|
||||||
baseCost: disc.baseCost,
|
.map((src) => `${src} mana`)
|
||||||
statBonus: disc.statBonus.stat,
|
: []}
|
||||||
statBonusLabel: disc.statBonus.label,
|
onToggle={handleToggle}
|
||||||
requires: disc.requires,
|
|
||||||
sourceManaTypes: disc.sourceManaTypes,
|
|
||||||
conversionRate: disc.conversionRate,
|
|
||||||
baseValue: disc.statBonus.baseValue,
|
|
||||||
drainBase: disc.drainBase,
|
|
||||||
difficultyFactor: disc.difficultyFactor,
|
|
||||||
scalingFactor: disc.scalingFactor,
|
|
||||||
}}
|
|
||||||
runtime={{
|
|
||||||
xp: discState.xp,
|
|
||||||
paused: discState.paused,
|
|
||||||
concurrentLimit,
|
|
||||||
isLocked: !prereqCheck.canProceed,
|
|
||||||
missingPrereqs: prereqCheck.missingPrereqs,
|
|
||||||
missingSourceMana: disc.sourceManaTypes
|
|
||||||
? disc.sourceManaTypes.filter(
|
|
||||||
(src) => src !== 'raw' && (!elements[src] || !elements[src].unlocked),
|
|
||||||
).map((src) => `${src} mana`)
|
|
||||||
: [],
|
|
||||||
}}
|
|
||||||
callbacks={{
|
|
||||||
onToggle: handleToggle,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary info */}
|
{/* Summary */}
|
||||||
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
|
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
|
||||||
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div>
|
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div>
|
||||||
<div>Concurrent Limit: {concurrentLimit}</div>
|
<div>Concurrent Limit: {concurrentLimit}</div>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// ─── Discipline Card Utility Functions ─────────────────────────────────────────
|
||||||
|
// Shared helpers for computing perk effects and stat bonus totals in the UI.
|
||||||
|
|
||||||
|
import type { DisciplinePerk } from '@/lib/game/types/disciplines';
|
||||||
|
import { calculatePerkTier } from '@/lib/game/utils/discipline-math';
|
||||||
|
import { TICK_MS } from '@/lib/game/constants/core';
|
||||||
|
|
||||||
|
// TICK_MS = 200, so 5 ticks per second
|
||||||
|
export const TICKS_PER_SECOND = 1000 / TICK_MS;
|
||||||
|
|
||||||
|
export interface ComputedPerkEffect {
|
||||||
|
description: string;
|
||||||
|
currentEffect: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the current effect for a perk based on XP.
|
||||||
|
* Returns a human-readable string like "+5.25/sec" or "+50 (unlocked)".
|
||||||
|
*/
|
||||||
|
export function computePerkCurrentEffect(
|
||||||
|
perk: DisciplinePerk,
|
||||||
|
xp: number,
|
||||||
|
): string {
|
||||||
|
if (xp < perk.threshold) {
|
||||||
|
return `at ${perk.threshold} XP`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!perk.bonus) {
|
||||||
|
return 'unlocked';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { amount } = perk.bonus;
|
||||||
|
|
||||||
|
switch (perk.type) {
|
||||||
|
case 'once':
|
||||||
|
return `+${amount} (unlocked)`;
|
||||||
|
case 'infinite': {
|
||||||
|
const tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||||
|
const total = tier * amount;
|
||||||
|
return `+${total.toFixed(2)}/sec`;
|
||||||
|
}
|
||||||
|
case 'capped': {
|
||||||
|
let tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||||
|
if (perk.maxTier !== undefined) {
|
||||||
|
tier = Math.min(tier, perk.maxTier);
|
||||||
|
}
|
||||||
|
const total = tier * amount;
|
||||||
|
return `+${total.toFixed(2)}/sec`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute total perk bonus amount for a specific statKey from all unlocked perks.
|
||||||
|
* Mirrors the logic in computeDisciplineEffects() for 'once', 'infinite', and 'capped' perks.
|
||||||
|
*/
|
||||||
|
export function computeTotalPerkBonusForStat(
|
||||||
|
perks: DisciplinePerk[],
|
||||||
|
xp: number,
|
||||||
|
statKey: string,
|
||||||
|
): number {
|
||||||
|
let total = 0;
|
||||||
|
for (const perk of perks) {
|
||||||
|
if (xp < perk.threshold) continue;
|
||||||
|
if (!perk.bonus || perk.bonus.stat !== statKey) continue;
|
||||||
|
|
||||||
|
switch (perk.type) {
|
||||||
|
case 'once':
|
||||||
|
total += perk.bonus.amount;
|
||||||
|
break;
|
||||||
|
case 'infinite': {
|
||||||
|
const tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||||
|
if (tier > 0) total += tier * perk.bonus.amount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'capped': {
|
||||||
|
let tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||||
|
if (tier > 0 && perk.maxTier !== undefined) {
|
||||||
|
tier = Math.min(tier, perk.maxTier);
|
||||||
|
}
|
||||||
|
if (tier > 0) total += tier * perk.bonus.amount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
|
|||||||
baseCost: 5,
|
baseCost: 5,
|
||||||
description:
|
description:
|
||||||
'Deepen your meditation practice to accelerate discipline XP gain. The more you master yourself, the faster all disciplines grow.',
|
'Deepen your meditation practice to accelerate discipline XP gain. The more you master yourself, the faster all disciplines grow.',
|
||||||
statBonus: { stat: 'disciplineXpBonus', baseValue: 0.5, label: 'Discipline XP Bonus/tick' },
|
statBonus: { stat: 'disciplineXpBonus', baseValue: 0.5, label: 'Discipline XP Bonus/sec' },
|
||||||
difficultyFactor: 120,
|
difficultyFactor: 120,
|
||||||
scalingFactor: 60,
|
scalingFactor: 60,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
@@ -94,7 +94,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
|
|||||||
type: 'once',
|
type: 'once',
|
||||||
threshold: 100,
|
threshold: 100,
|
||||||
value: 0,
|
value: 0,
|
||||||
description: '+0.5 Discipline XP per tick',
|
description: '+0.5 Discipline XP per sec',
|
||||||
bonus: { stat: 'disciplineXpBonus', amount: 0.5 },
|
bonus: { stat: 'disciplineXpBonus', amount: 0.5 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,7 +102,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
|
|||||||
type: 'infinite',
|
type: 'infinite',
|
||||||
threshold: 200,
|
threshold: 200,
|
||||||
value: 100,
|
value: 100,
|
||||||
description: 'Every 100 XP: +0.25 Discipline XP per tick',
|
description: 'Every 100 XP: +0.25 Discipline XP per sec',
|
||||||
bonus: { stat: 'disciplineXpBonus', amount: 0.25 },
|
bonus: { stat: 'disciplineXpBonus', amount: 0.25 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user