Files
Mana-Loop/src/lib/game/store/skillSlice.ts
Z User b78c979647
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled
Redesign skill system with upgrade trees and tier progression
Major changes:
- Created docs/skills.md with comprehensive skill system documentation
- Rewrote skill-evolution.ts with new upgrade tree structure:
  - Upgrades organized in branching paths with prerequisites
  - Each choice can lead to upgraded versions at future milestones
  - Support for upgrade children and requirement chains
- Added getBaseSkillId and generateTierSkillDef helper functions
- Fixed getFloorElement to use FLOOR_ELEM_CYCLE.length
- Updated test files to match current skill definitions
- Removed tests for non-existent skills

Skill system now supports:
- Levels 1-10 for most skills, level 5 caps for specialized, level 1 for research
- Tier up system: Tier N Level 1 = Tier N-1 Level 10 in power
- Milestone upgrades at levels 5 and 10 with branching upgrade trees
- Attunement requirements for skill access and tier up
- Study costs and time for leveling
2026-04-03 11:08:58 +00:00

347 lines
10 KiB
TypeScript
Executable File

// ─── Skill Slice ──────────────────────────────────────────────────────────────
// Manages skills, studying, and skill progress
import type { StateCreator } from 'zustand';
import type { GameState, StudyTarget, SkillUpgradeChoice } from '../types';
import { SKILLS_DEF, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '../skill-evolution';
import { computeEffects } from '../upgrade-effects';
export interface SkillSlice {
// State
skills: Record<string, number>;
skillProgress: Record<string, number>;
skillUpgrades: Record<string, string[]>;
skillTiers: Record<string, number>;
currentStudyTarget: StudyTarget | null;
parallelStudyTarget: StudyTarget | null;
// Actions
startStudyingSkill: (skillId: string) => void;
startStudyingSpell: (spellId: string) => void;
startParallelStudySkill: (skillId: string) => void;
cancelStudy: () => void;
cancelParallelStudy: () => void;
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => void;
tierUpSkill: (skillId: string) => void;
// Getters
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
}
export const createSkillSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): SkillSlice => ({
skills: {},
skillProgress: {},
skillUpgrades: {},
skillTiers: {},
currentStudyTarget: null,
parallelStudyTarget: null,
startStudyingSkill: (skillId: string) => {
const state = get();
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Check prerequisites
if (sk.req) {
for (const [r, rl] of Object.entries(sk.req)) {
if ((state.skills[r] || 0) < rl) return;
}
}
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
const manaCostPerHour = totalCost / sk.studyTime;
set({
currentAction: 'study',
currentStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour,
},
log: [`📚 Started studying ${sk.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
});
},
startStudyingSpell: (spellId: string) => {
const state = get();
const sp = SPELLS_DEF[spellId];
if (!sp || state.spells[spellId]?.learned) return;
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sp.unlock * costMult);
const studyTime = sp.studyTime || (sp.tier * 4);
const manaCostPerHour = totalCost / studyTime;
set({
currentAction: 'study',
currentStudyTarget: {
type: 'spell',
id: spellId,
progress: state.spells[spellId]?.studyProgress || 0,
required: studyTime,
manaCostPerHour,
},
spells: {
...state.spells,
[spellId]: {
...(state.spells[spellId] || { learned: false, level: 0 }),
studyProgress: state.spells[spellId]?.studyProgress || 0,
},
},
log: [`📚 Started studying ${sp.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
});
},
startParallelStudySkill: (skillId: string) => {
const state = get();
if (state.parallelStudyTarget) return;
if (!state.currentStudyTarget) return;
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
if (state.currentStudyTarget.id === skillId) return;
set({
parallelStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour: 0, // Parallel study doesn't cost extra
},
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
});
},
cancelStudy: () => {
const state = get();
if (!state.currentStudyTarget) return;
const savedProgress = state.currentStudyTarget.progress;
const log = ['📖 Study paused. Progress saved.', ...state.log.slice(0, 49)];
if (state.currentStudyTarget.type === 'skill') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: {
...state.skillProgress,
[state.currentStudyTarget.id]: savedProgress,
},
log,
});
} else {
set({
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...state.spells,
[state.currentStudyTarget.id]: {
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
studyProgress: savedProgress,
},
},
log,
});
}
},
cancelParallelStudy: () => {
set((state) => {
if (!state.parallelStudyTarget) return state;
return {
parallelStudyTarget: null,
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
};
});
},
selectSkillUpgrade: (skillId: string, upgradeId: string) => {
set((state) => {
const current = state.skillUpgrades?.[skillId] || [];
if (current.includes(upgradeId)) return state;
if (current.length >= 2) return state;
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: [...current, upgradeId],
},
};
});
},
deselectSkillUpgrade: (skillId: string, upgradeId: string) => {
set((state) => {
const current = state.skillUpgrades?.[skillId] || [];
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: current.filter(id => id !== upgradeId),
},
};
});
},
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => {
set((state) => {
const existingUpgrades = state.skillUpgrades?.[skillId] || [];
const otherMilestoneUpgrades = existingUpgrades.filter(
id => milestone === 5 ? id.includes('_l10') : id.includes('_l5')
);
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: [...otherMilestoneUpgrades, ...upgradeIds],
},
};
});
},
tierUpSkill: (skillId: string) => {
const state = get();
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
const currentTier = state.skillTiers?.[baseSkillId] || 1;
const nextTier = currentTier + 1;
if (nextTier > 5) return;
const nextTierSkillId = `${baseSkillId}_t${nextTier}`;
set({
skillTiers: {
...state.skillTiers,
[baseSkillId]: nextTier,
},
skills: {
...state.skills,
[nextTierSkillId]: 0,
[skillId]: 0,
},
skillProgress: {
...state.skillProgress,
[skillId]: 0,
[nextTierSkillId]: 0,
},
skillUpgrades: {
...state.skillUpgrades,
[nextTierSkillId]: [],
[skillId]: [],
},
log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)],
});
},
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
const state = get();
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
const tier = state.skillTiers?.[baseSkillId] || 1;
const available = getUpgradesForSkillAtMilestone(skillId, milestone, state.skillTiers || {});
const selected = (state.skillUpgrades?.[skillId] || []).filter(id =>
available.some(u => u.id === id)
);
return { available, selected };
},
});
// Process study progress (called during tick)
export function processStudy(state: GameState, deltaHours: number): Partial<GameState> {
if (state.currentAction !== 'study' || !state.currentStudyTarget) return {};
const target = state.currentStudyTarget;
const studySpeedMult = getStudySpeedMultiplier(state.skills);
const progressGain = deltaHours * studySpeedMult;
const manaCost = progressGain * target.manaCostPerHour;
let rawMana = state.rawMana;
let totalManaGathered = state.totalManaGathered;
let skills = state.skills;
let skillProgress = state.skillProgress;
let spells = state.spells;
const log = [...state.log];
if (rawMana >= manaCost) {
rawMana -= manaCost;
totalManaGathered += manaCost;
const newProgress = target.progress + progressGain;
if (newProgress >= target.required) {
// Study complete
if (target.type === 'skill') {
const skillId = target.id;
const currentLevel = skills[skillId] || 0;
skills = { ...skills, [skillId]: currentLevel + 1 };
skillProgress = { ...skillProgress, [skillId]: 0 };
log.unshift(`${SKILLS_DEF[skillId]?.name} Lv.${currentLevel + 1} mastered!`);
} else if (target.type === 'spell') {
const spellId = target.id;
spells = {
...spells,
[spellId]: { learned: true, level: 1, studyProgress: 0 },
};
log.unshift(`📖 ${SPELLS_DEF[spellId]?.name} learned!`);
}
return {
rawMana,
totalManaGathered,
skills,
skillProgress,
spells,
currentStudyTarget: null,
currentAction: 'meditate',
log,
};
}
return {
rawMana,
totalManaGathered,
currentStudyTarget: { ...target, progress: newProgress },
};
}
// Not enough mana
log.unshift('⚠️ Not enough mana to continue studying. Progress saved.');
if (target.type === 'skill') {
return {
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: { ...skillProgress, [target.id]: target.progress },
log,
};
} else {
return {
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...spells,
[target.id]: {
...(spells[target.id] || { learned: false, level: 0 }),
studyProgress: target.progress,
},
},
log,
};
}
}