This commit is contained in:
Z User
2026-03-25 16:35:56 +00:00
parent 3b2e89db74
commit 7c5f2f30f0
11 changed files with 2929 additions and 10 deletions

View File

@@ -0,0 +1,272 @@
// ─── Achievement Definitions ───────────────────────────────────────────────────
import type { AchievementDef } from '../types';
export const ACHIEVEMENTS: Record<string, AchievementDef> = {
// ─── Combat Achievements ───
firstBlood: {
id: 'firstBlood',
name: 'First Blood',
desc: 'Clear your first floor',
category: 'combat',
requirement: { type: 'floor', value: 2 },
reward: { insight: 10 },
},
floorClimber: {
id: 'floorClimber',
name: 'Floor Climber',
desc: 'Reach floor 10',
category: 'combat',
requirement: { type: 'floor', value: 10 },
reward: { insight: 25, manaBonus: 10 },
},
spireAssault: {
id: 'spireAssault',
name: 'Spire Assault',
desc: 'Reach floor 25',
category: 'combat',
requirement: { type: 'floor', value: 25 },
reward: { insight: 50, damageBonus: 0.05 },
},
towerConqueror: {
id: 'towerConqueror',
name: 'Tower Conqueror',
desc: 'Reach floor 50',
category: 'combat',
requirement: { type: 'floor', value: 50 },
reward: { insight: 100, manaBonus: 50, damageBonus: 0.1 },
},
spireMaster: {
id: 'spireMaster',
name: 'Spire Master',
desc: 'Reach floor 75',
category: 'combat',
requirement: { type: 'floor', value: 75 },
reward: { insight: 200, damageBonus: 0.15, title: 'Spire Master' },
},
apexReached: {
id: 'apexReached',
name: 'Apex Reached',
desc: 'Reach floor 100',
category: 'combat',
requirement: { type: 'floor', value: 100 },
reward: { insight: 500, manaBonus: 200, damageBonus: 0.25, title: 'Apex Climber' },
},
// ─── Combo Achievements ───
comboStarter: {
id: 'comboStarter',
name: 'Combo Starter',
desc: 'Reach a 10-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 10 },
reward: { insight: 15 },
},
comboMaster: {
id: 'comboMaster',
name: 'Combo Master',
desc: 'Reach a 50-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 50 },
reward: { insight: 50, damageBonus: 0.05 },
},
comboLegend: {
id: 'comboLegend',
name: 'Combo Legend',
desc: 'Reach a 100-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 100 },
reward: { insight: 150, damageBonus: 0.1, title: 'Combo Legend' },
},
// ─── Damage Achievements ───
hundredDamage: {
id: 'hundredDamage',
name: 'Heavy Hitter',
desc: 'Deal 100 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 100 },
reward: { insight: 20 },
},
thousandDamage: {
id: 'thousandDamage',
name: 'Devastating Blow',
desc: 'Deal 1,000 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 1000 },
reward: { insight: 75, damageBonus: 0.03 },
},
tenThousandDamage: {
id: 'tenThousandDamage',
name: 'Apocalypse Now',
desc: 'Deal 10,000 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 10000 },
reward: { insight: 200, damageBonus: 0.05, title: 'Apocalypse Bringer' },
},
// ─── Pact Achievements ───
pactSeeker: {
id: 'pactSeeker',
name: 'Pact Seeker',
desc: 'Sign your first guardian pact',
category: 'progression',
requirement: { type: 'pact', value: 1 },
reward: { insight: 30 },
},
pactCollector: {
id: 'pactCollector',
name: 'Pact Collector',
desc: 'Sign 5 guardian pacts',
category: 'progression',
requirement: { type: 'pact', value: 5 },
reward: { insight: 100, manaBonus: 25 },
},
pactMaster: {
id: 'pactMaster',
name: 'Pact Master',
desc: 'Sign all guardian pacts',
category: 'progression',
requirement: { type: 'pact', value: 12 },
reward: { insight: 500, damageBonus: 0.2, title: 'Pact Master' },
},
// ─── Magic Achievements ───
spellCaster: {
id: 'spellCaster',
name: 'Spell Caster',
desc: 'Cast 100 spells',
category: 'magic',
requirement: { type: 'spells', value: 100 },
reward: { insight: 25 },
},
spellWeaver: {
id: 'spellWeaver',
name: 'Spell Weaver',
desc: 'Cast 1,000 spells',
category: 'magic',
requirement: { type: 'spells', value: 1000 },
reward: { insight: 75, regenBonus: 0.5 },
},
spellStorm: {
id: 'spellStorm',
name: 'Spell Storm',
desc: 'Cast 10,000 spells',
category: 'magic',
requirement: { type: 'spells', value: 10000 },
reward: { insight: 200, regenBonus: 1, title: 'Storm Caller' },
},
// ─── Mana Achievements ───
manaPool: {
id: 'manaPool',
name: 'Mana Pool',
desc: 'Accumulate 1,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 1000 },
reward: { insight: 20 },
},
manaLake: {
id: 'manaLake',
name: 'Mana Lake',
desc: 'Accumulate 100,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 100000 },
reward: { insight: 100, manaBonus: 50 },
},
manaOcean: {
id: 'manaOcean',
name: 'Mana Ocean',
desc: 'Accumulate 10,000,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 10000000 },
reward: { insight: 500, manaBonus: 200, title: 'Mana Ocean' },
},
// ─── Crafting Achievements ───
enchanter: {
id: 'enchanter',
name: 'Enchanter',
desc: 'Complete your first enchantment',
category: 'crafting',
requirement: { type: 'craft', value: 1 },
reward: { insight: 30 },
},
masterEnchanter: {
id: 'masterEnchanter',
name: 'Master Enchanter',
desc: 'Complete 10 enchantments',
category: 'crafting',
requirement: { type: 'craft', value: 10 },
reward: { insight: 100, unlockEffect: 'efficiencyBoost' },
},
legendaryEnchanter: {
id: 'legendaryEnchanter',
name: 'Legendary Enchanter',
desc: 'Complete 50 enchantments',
category: 'crafting',
requirement: { type: 'craft', value: 50 },
reward: { insight: 300, title: 'Legendary Enchanter' },
},
// ─── Special Achievements ───
speedRunner: {
id: 'speedRunner',
name: 'Speed Runner',
desc: 'Reach floor 50 in under 5 days',
category: 'special',
requirement: { type: 'time', value: 5, subType: 'floor50' },
reward: { insight: 200, title: 'Speed Demon' },
hidden: true,
},
perfectionist: {
id: 'perfectionist',
name: 'Perfectionist',
desc: 'Reach floor 100 without any guardian pacts',
category: 'special',
requirement: { type: 'floor', value: 100, subType: 'noPacts' },
reward: { insight: 1000, title: 'Perfect Climber' },
hidden: true,
},
survivor: {
id: 'survivor',
name: 'Survivor',
desc: 'Complete a loop during full incursion (day 30+)',
category: 'special',
requirement: { type: 'time', value: 30 },
reward: { insight: 300, manaBonus: 100, title: 'Survivor' },
},
};
// Category colors for UI
export const ACHIEVEMENT_CATEGORY_COLORS: Record<string, string> = {
combat: '#EF4444', // Red
progression: '#F59E0B', // Amber
crafting: '#8B5CF6', // Purple
magic: '#3B82F6', // Blue
special: '#EC4899', // Pink
};
// Get achievements by category
export function getAchievementsByCategory(): Record<string, AchievementDef[]> {
const result: Record<string, AchievementDef[]> = {};
for (const achievement of Object.values(ACHIEVEMENTS)) {
if (!result[achievement.category]) {
result[achievement.category] = [];
}
result[achievement.category].push(achievement);
}
return result;
}
// Check if an achievement should be revealed
export function isAchievementRevealed(
achievement: AchievementDef,
progress: number
): boolean {
if (!achievement.hidden) return true;
// Reveal hidden achievements when at 50% progress
return progress >= achievement.requirement.value * 0.5;
}

View File

@@ -0,0 +1,498 @@
// ─── Familiar Definitions ───────────────────────────────────────────────────────
// Magical companions that provide passive bonuses and active assistance
import type { FamiliarDef, FamiliarAbility } from '../types';
// ─── Familiar Abilities ─────────────────────────────────────────────────────────
const ABILITIES = {
// Combat abilities
damageBonus: (base: number, scaling: number): FamiliarAbility => ({
type: 'damageBonus',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% damage (+${scaling}% per level)`,
}),
critChance: (base: number, scaling: number): FamiliarAbility => ({
type: 'critChance',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% crit chance (+${scaling}% per level)`,
}),
castSpeed: (base: number, scaling: number): FamiliarAbility => ({
type: 'castSpeed',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% cast speed (+${scaling}% per level)`,
}),
elementalBonus: (base: number, scaling: number): FamiliarAbility => ({
type: 'elementalBonus',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% elemental damage (+${scaling}% per level)`,
}),
lifeSteal: (base: number, scaling: number): FamiliarAbility => ({
type: 'lifeSteal',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% life steal (+${scaling}% per level)`,
}),
thorns: (base: number, scaling: number): FamiliarAbility => ({
type: 'thorns',
baseValue: base,
scalingPerLevel: scaling,
desc: `Reflect ${base}% damage taken (+${scaling}% per level)`,
}),
// Mana abilities
manaRegen: (base: number, scaling: number): FamiliarAbility => ({
type: 'manaRegen',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base} mana regen (+${scaling} per level)`,
}),
autoGather: (base: number, scaling: number): FamiliarAbility => ({
type: 'autoGather',
baseValue: base,
scalingPerLevel: scaling,
desc: `Auto-gather ${base} mana/hour (+${scaling} per level)`,
}),
autoConvert: (base: number, scaling: number): FamiliarAbility => ({
type: 'autoConvert',
baseValue: base,
scalingPerLevel: scaling,
desc: `Auto-convert ${base} mana/hour (+${scaling} per level)`,
}),
manaShield: (base: number, scaling: number): FamiliarAbility => ({
type: 'manaShield',
baseValue: base,
scalingPerLevel: scaling,
desc: `Shield absorbs ${base} damage, costs 1 mana per ${base} damage`,
}),
// Support abilities
bonusGold: (base: number, scaling: number): FamiliarAbility => ({
type: 'bonusGold',
baseValue: base,
scalingPerLevel: scaling,
desc: `+${base}% insight gain (+${scaling}% per level)`,
}),
};
// ─── Familiar Definitions ───────────────────────────────────────────────────────
export const FAMILIARS_DEF: Record<string, FamiliarDef> = {
// === COMMON FAMILIARS (Tier 1) ===
// Mana Wisps - Basic mana helpers
manaWisp: {
id: 'manaWisp',
name: 'Mana Wisp',
desc: 'A gentle spirit of pure mana that drifts lazily through the air.',
role: 'mana',
element: 'raw',
rarity: 'common',
abilities: [
ABILITIES.manaRegen(0.5, 0.1),
],
baseStats: { power: 10, bond: 15 },
unlockCondition: { type: 'mana', value: 100 },
flavorText: 'It hums with quiet contentment, barely visible in dim light.',
},
fireSpark: {
id: 'fireSpark',
name: 'Fire Spark',
desc: 'A tiny ember given life, crackling with barely contained energy.',
role: 'combat',
element: 'fire',
rarity: 'common',
abilities: [
ABILITIES.damageBonus(2, 0.5),
],
baseStats: { power: 12, bond: 10 },
unlockCondition: { type: 'floor', value: 5 },
flavorText: 'It bounces excitedly, leaving scorch marks on everything it touches.',
},
waterDroplet: {
id: 'waterDroplet',
name: 'Water Droplet',
desc: 'A perfect sphere of living water that never seems to evaporate.',
role: 'support',
element: 'water',
rarity: 'common',
abilities: [
ABILITIES.manaRegen(0.3, 0.1),
ABILITIES.lifeSteal(1, 0.2),
],
baseStats: { power: 8, bond: 12 },
unlockCondition: { type: 'floor', value: 3 },
flavorText: 'Ripples spread across its surface with each spell you cast.',
},
earthPebble: {
id: 'earthPebble',
name: 'Earth Pebble',
desc: 'A small stone with a surprisingly friendly personality.',
role: 'guardian',
element: 'earth',
rarity: 'common',
abilities: [
ABILITIES.thorns(2, 0.5),
],
baseStats: { power: 15, bond: 8 },
unlockCondition: { type: 'floor', value: 8 },
flavorText: 'It occasionally rolls itself to a new position when bored.',
},
// === UNCOMMON FAMILIARS (Tier 2) ===
flameImp: {
id: 'flameImp',
name: 'Flame Imp',
desc: 'A mischievous fire spirit that delights in destruction.',
role: 'combat',
element: 'fire',
rarity: 'uncommon',
abilities: [
ABILITIES.damageBonus(4, 0.8),
ABILITIES.elementalBonus(3, 0.6),
],
baseStats: { power: 25, bond: 12 },
unlockCondition: { type: 'floor', value: 15 },
flavorText: 'It cackles with glee whenever you defeat an enemy.',
},
windSylph: {
id: 'windSylph',
name: 'Wind Sylph',
desc: 'An airy spirit that moves like a gentle breeze.',
role: 'support',
element: 'air',
rarity: 'uncommon',
abilities: [
ABILITIES.castSpeed(3, 0.6),
],
baseStats: { power: 20, bond: 15 },
unlockCondition: { type: 'floor', value: 12 },
flavorText: 'Its laughter sounds like wind chimes in a storm.',
},
manaSprite: {
id: 'manaSprite',
name: 'Mana Sprite',
desc: 'A more evolved mana spirit with a playful nature.',
role: 'mana',
element: 'raw',
rarity: 'uncommon',
abilities: [
ABILITIES.manaRegen(1, 0.2),
ABILITIES.autoGather(2, 0.5),
],
baseStats: { power: 18, bond: 18 },
unlockCondition: { type: 'mana', value: 1000 },
flavorText: 'It sometimes tickles your ear with invisible hands.',
},
crystalGolem: {
id: 'crystalGolem',
name: 'Crystal Golem',
desc: 'A small construct made of crystallized mana.',
role: 'guardian',
element: 'crystal',
rarity: 'uncommon',
abilities: [
ABILITIES.thorns(5, 1),
ABILITIES.manaShield(10, 2),
],
baseStats: { power: 30, bond: 10 },
unlockCondition: { type: 'floor', value: 20 },
flavorText: 'Light refracts through its body in mesmerizing patterns.',
},
// === RARE FAMILIARS (Tier 3) ===
phoenixHatchling: {
id: 'phoenixHatchling',
name: 'Phoenix Hatchling',
desc: 'A young phoenix, still learning to control its flames.',
role: 'combat',
element: 'fire',
rarity: 'rare',
abilities: [
ABILITIES.damageBonus(6, 1.2),
ABILITIES.lifeSteal(3, 0.5),
],
baseStats: { power: 40, bond: 15 },
unlockCondition: { type: 'floor', value: 30 },
flavorText: 'Tiny flames dance around its feathers as it practices flying.',
},
frostWisp: {
id: 'frostWisp',
name: 'Frost Wisp',
desc: 'A spirit of eternal winter, beautiful and deadly.',
role: 'combat',
element: 'water',
rarity: 'rare',
abilities: [
ABILITIES.elementalBonus(8, 1.5),
ABILITIES.castSpeed(4, 0.8),
],
baseStats: { power: 35, bond: 12 },
unlockCondition: { type: 'floor', value: 25 },
flavorText: 'Frost patterns appear on surfaces wherever it lingers.',
},
manaElemental: {
id: 'manaElemental',
name: 'Mana Elemental',
desc: 'A concentrated form of pure magical energy.',
role: 'mana',
element: 'raw',
rarity: 'rare',
abilities: [
ABILITIES.manaRegen(2, 0.4),
ABILITIES.autoGather(5, 1),
ABILITIES.autoConvert(2, 0.5),
],
baseStats: { power: 30, bond: 20 },
unlockCondition: { type: 'mana', value: 5000 },
flavorText: 'Reality seems to bend slightly around its fluctuating form.',
},
shieldGuardian: {
id: 'shieldGuardian',
name: 'Shield Guardian',
desc: 'A loyal protector carved from enchanted stone.',
role: 'guardian',
element: 'earth',
rarity: 'rare',
abilities: [
ABILITIES.thorns(8, 1.5),
ABILITIES.manaShield(20, 4),
],
baseStats: { power: 50, bond: 8 },
unlockCondition: { type: 'floor', value: 35 },
flavorText: 'It stands motionless for hours, then suddenly moves to block danger.',
},
// === EPIC FAMILIARS (Tier 4) ===
infernoDrake: {
id: 'infernoDrake',
name: 'Inferno Drake',
desc: 'A small dragon wreathed in eternal flames.',
role: 'combat',
element: 'fire',
rarity: 'epic',
abilities: [
ABILITIES.damageBonus(10, 2),
ABILITIES.elementalBonus(12, 2),
ABILITIES.critChance(3, 0.6),
],
baseStats: { power: 60, bond: 12 },
unlockCondition: { type: 'floor', value: 50 },
flavorText: 'Smoke occasionally drifts from its nostrils as it dreams of conquest.',
},
starlightSerpent: {
id: 'starlightSerpent',
name: 'Starlight Serpent',
desc: 'A serpentine creature formed from captured starlight.',
role: 'support',
element: 'stellar',
rarity: 'epic',
abilities: [
ABILITIES.castSpeed(8, 1.5),
ABILITIES.bonusGold(5, 1),
ABILITIES.manaRegen(1.5, 0.3),
],
baseStats: { power: 45, bond: 25 },
unlockCondition: { type: 'floor', value: 45 },
flavorText: 'It traces constellations in the air with its glowing body.',
},
voidWalker: {
id: 'voidWalker',
name: 'Void Walker',
desc: 'A being that exists partially outside normal reality.',
role: 'mana',
element: 'void',
rarity: 'epic',
abilities: [
ABILITIES.manaRegen(3, 0.6),
ABILITIES.autoGather(10, 2),
ABILITIES.manaShield(15, 3),
],
baseStats: { power: 55, bond: 15 },
unlockCondition: { type: 'floor', value: 55 },
flavorText: 'It sometimes disappears entirely, only to reappear moments later.',
},
ancientGolem: {
id: 'ancientGolem',
name: 'Ancient Golem',
desc: 'A construct from a forgotten age, still following its prime directive.',
role: 'guardian',
element: 'earth',
rarity: 'epic',
abilities: [
ABILITIES.thorns(15, 3),
ABILITIES.manaShield(30, 5),
ABILITIES.damageBonus(5, 1),
],
baseStats: { power: 80, bond: 6 },
unlockCondition: { type: 'floor', value: 60 },
flavorText: 'Ancient runes glow faintly across its weathered surface.',
},
// === LEGENDARY FAMILIARS (Tier 5) ===
primordialPhoenix: {
id: 'primordialPhoenix',
name: 'Primordial Phoenix',
desc: 'An ancient fire bird, reborn countless times through the ages.',
role: 'combat',
element: 'fire',
rarity: 'legendary',
abilities: [
ABILITIES.damageBonus(15, 3),
ABILITIES.elementalBonus(20, 4),
ABILITIES.lifeSteal(8, 1.5),
ABILITIES.critChance(5, 1),
],
baseStats: { power: 100, bond: 20 },
unlockCondition: { type: 'pact', value: 25 }, // Guardian floor 25
flavorText: 'Its eyes hold the wisdom of a thousand lifetimes.',
},
leviathanSpawn: {
id: 'leviathanSpawn',
name: 'Leviathan Spawn',
desc: 'The offspring of an ancient sea god, still growing into its power.',
role: 'mana',
element: 'water',
rarity: 'legendary',
abilities: [
ABILITIES.manaRegen(5, 1),
ABILITIES.autoGather(20, 4),
ABILITIES.autoConvert(8, 1.5),
ABILITIES.manaShield(25, 5),
],
baseStats: { power: 90, bond: 18 },
unlockCondition: { type: 'pact', value: 50 },
flavorText: 'The air around it always smells of salt and deep ocean.',
},
celestialGuardian: {
id: 'celestialGuardian',
name: 'Celestial Guardian',
desc: 'A divine protector sent by powers beyond mortal comprehension.',
role: 'guardian',
element: 'light',
rarity: 'legendary',
abilities: [
ABILITIES.thorns(25, 5),
ABILITIES.manaShield(50, 10),
ABILITIES.damageBonus(10, 2),
ABILITIES.lifeSteal(5, 1),
],
baseStats: { power: 120, bond: 12 },
unlockCondition: { type: 'pact', value: 75 },
flavorText: 'It radiates an aura of absolute protection and quiet judgment.',
},
voidEmperor: {
id: 'voidEmperor',
name: 'Void Emperor',
desc: 'A ruler from the spaces between dimensions, bound to your service.',
role: 'support',
element: 'void',
rarity: 'legendary',
abilities: [
ABILITIES.castSpeed(15, 3),
ABILITIES.bonusGold(15, 3),
ABILITIES.manaRegen(4, 0.8),
ABILITIES.critChance(8, 1.5),
],
baseStats: { power: 85, bond: 25 },
unlockCondition: { type: 'floor', value: 90 },
flavorText: 'It regards reality with the detached interest of a god.',
},
};
// ─── Helper Functions ───────────────────────────────────────────────────────────
// Get XP required for next familiar level
export function getFamiliarXpRequired(level: number): number {
// Exponential scaling: 100 * 1.5^(level-1)
return Math.floor(100 * Math.pow(1.5, level - 1));
}
// Get bond required for next bond level (1-100)
export function getBondRequired(currentBond: number): number {
// Linear scaling, every 10 bond requires more time
const bondTier = Math.floor(currentBond / 10);
return 100 + bondTier * 50; // Base 100, +50 per tier
}
// Calculate familiar's ability value at given level and ability level
export function getFamiliarAbilityValue(
ability: FamiliarAbility,
familiarLevel: number,
abilityLevel: number
): number {
// Base value + (familiar level bonus) + (ability level bonus)
const familiarBonus = Math.floor(familiarLevel / 10) * ability.scalingPerLevel;
const abilityBonus = (abilityLevel - 1) * ability.scalingPerLevel * 2;
return ability.baseValue + familiarBonus + abilityBonus;
}
// Get all familiars of a specific rarity
export function getFamiliarsByRarity(rarity: FamiliarDef['rarity']): FamiliarDef[] {
return Object.values(FAMILIARS_DEF).filter(f => f.rarity === rarity);
}
// Get all familiars of a specific role
export function getFamiliarsByRole(role: FamiliarRole): FamiliarDef[] {
return Object.values(FAMILIARS_DEF).filter(f => f.role === role);
}
// Check if player meets unlock condition for a familiar
export function canUnlockFamiliar(
familiar: FamiliarDef,
maxFloor: number,
signedPacts: number[],
totalManaGathered: number,
skillsLearned: number
): boolean {
if (!familiar.unlockCondition) return true;
const { type, value } = familiar.unlockCondition;
switch (type) {
case 'floor':
return maxFloor >= value;
case 'pact':
return signedPacts.includes(value);
case 'mana':
return totalManaGathered >= value;
case 'study':
return skillsLearned >= value;
default:
return false;
}
}
// Starting familiar (given to new players)
export const STARTING_FAMILIAR = 'manaWisp';

View File

@@ -0,0 +1,242 @@
// ─── Loot Drop Definitions ─────────────────────────────────────────────────────
import type { LootDrop } from '../types';
export const LOOT_DROPS: Record<string, LootDrop> = {
// ─── Materials (used for crafting) ───
manaCrystalDust: {
id: 'manaCrystalDust',
name: 'Mana Crystal Dust',
rarity: 'common',
type: 'material',
minFloor: 1,
dropChance: 0.15,
},
arcaneShard: {
id: 'arcaneShard',
name: 'Arcane Shard',
rarity: 'uncommon',
type: 'material',
minFloor: 10,
dropChance: 0.10,
},
elementalCore: {
id: 'elementalCore',
name: 'Elemental Core',
rarity: 'rare',
type: 'material',
minFloor: 25,
dropChance: 0.08,
},
voidEssence: {
id: 'voidEssence',
name: 'Void Essence',
rarity: 'epic',
type: 'material',
minFloor: 50,
dropChance: 0.05,
guardianOnly: true,
},
celestialFragment: {
id: 'celestialFragment',
name: 'Celestial Fragment',
rarity: 'legendary',
type: 'material',
minFloor: 75,
dropChance: 0.02,
guardianOnly: true,
},
// ─── Elemental Essence (grants elemental mana) ───
fireEssenceDrop: {
id: 'fireEssenceDrop',
name: 'Fire Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
waterEssenceDrop: {
id: 'waterEssenceDrop',
name: 'Water Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
airEssenceDrop: {
id: 'airEssenceDrop',
name: 'Air Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
earthEssenceDrop: {
id: 'earthEssenceDrop',
name: 'Earth Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
lightEssenceDrop: {
id: 'lightEssenceDrop',
name: 'Light Essence',
rarity: 'rare',
type: 'essence',
minFloor: 20,
dropChance: 0.08,
amount: { min: 3, max: 10 },
},
darkEssenceDrop: {
id: 'darkEssenceDrop',
name: 'Dark Essence',
rarity: 'rare',
type: 'essence',
minFloor: 20,
dropChance: 0.08,
amount: { min: 3, max: 10 },
},
lifeEssenceDrop: {
id: 'lifeEssenceDrop',
name: 'Life Essence',
rarity: 'epic',
type: 'essence',
minFloor: 40,
dropChance: 0.05,
amount: { min: 2, max: 8 },
},
deathEssenceDrop: {
id: 'deathEssenceDrop',
name: 'Death Essence',
rarity: 'epic',
type: 'essence',
minFloor: 40,
dropChance: 0.05,
amount: { min: 2, max: 8 },
},
// ─── Raw Mana Drops ───
manaOrb: {
id: 'manaOrb',
name: 'Mana Orb',
rarity: 'common',
type: 'gold', // Uses gold type but gives raw mana
minFloor: 1,
dropChance: 0.20,
amount: { min: 10, max: 50 },
},
greaterManaOrb: {
id: 'greaterManaOrb',
name: 'Greater Mana Orb',
rarity: 'uncommon',
type: 'gold',
minFloor: 15,
dropChance: 0.10,
amount: { min: 50, max: 150 },
},
supremeManaOrb: {
id: 'supremeManaOrb',
name: 'Supreme Mana Orb',
rarity: 'rare',
type: 'gold',
minFloor: 35,
dropChance: 0.05,
amount: { min: 100, max: 500 },
},
// ─── Equipment Blueprints ───
staffBlueprint: {
id: 'staffBlueprint',
name: 'Staff Blueprint',
rarity: 'uncommon',
type: 'blueprint',
minFloor: 10,
dropChance: 0.03,
},
wandBlueprint: {
id: 'wandBlueprint',
name: 'Wand Blueprint',
rarity: 'rare',
type: 'blueprint',
minFloor: 20,
dropChance: 0.02,
},
robeBlueprint: {
id: 'robeBlueprint',
name: 'Mage Robe Blueprint',
rarity: 'rare',
type: 'blueprint',
minFloor: 25,
dropChance: 0.02,
},
artifactBlueprint: {
id: 'artifactBlueprint',
name: 'Artifact Blueprint',
rarity: 'legendary',
type: 'blueprint',
minFloor: 60,
dropChance: 0.01,
guardianOnly: true,
},
};
// Rarity colors for UI
export const RARITY_COLORS: Record<string, { color: string; glow: string }> = {
common: { color: '#9CA3AF', glow: '#9CA3AF40' },
uncommon: { color: '#22C55E', glow: '#22C55E40' },
rare: { color: '#3B82F6', glow: '#3B82F640' },
epic: { color: '#A855F7', glow: '#A855F740' },
legendary: { color: '#F59E0B', glow: '#F59E0B60' },
};
// Get loot drops available at a given floor
export function getAvailableDrops(floor: number, isGuardian: boolean): LootDrop[] {
return Object.values(LOOT_DROPS).filter(drop => {
if (drop.minFloor > floor) return false;
if (drop.guardianOnly && !isGuardian) return false;
return true;
});
}
// Roll for loot drops
export function rollLootDrops(
floor: number,
isGuardian: boolean,
luckBonus: number = 0
): Array<{ drop: LootDrop; amount: number }> {
const available = getAvailableDrops(floor, isGuardian);
const drops: Array<{ drop: LootDrop; amount: number }> = [];
for (const drop of available) {
// Calculate adjusted drop chance
let chance = drop.dropChance;
chance *= (1 + luckBonus); // Apply luck bonus
// Guardian floors have 2x drop rate
if (isGuardian) chance *= 2;
// Cap at 50% for any single drop
chance = Math.min(0.5, chance);
if (Math.random() < chance) {
let amount = 1;
// For gold/essence types, roll amount
if (drop.amount) {
amount = Math.floor(
Math.random() * (drop.amount.max - drop.amount.min + 1) + drop.amount.min
);
}
drops.push({ drop, amount });
}
}
return drops;
}

View File

@@ -0,0 +1,367 @@
// ─── Familiar Slice ─────────────────────────────────────────────────────────────
// Actions and computations for the familiar system
import type { GameState, FamiliarInstance, FamiliarAbilityType } from './types';
import { FAMILIARS_DEF, getFamiliarXpRequired, getFamiliarAbilityValue, canUnlockFamiliar, STARTING_FAMILIAR } from './data/familiars';
import { HOURS_PER_TICK } from './constants';
// ─── Familiar Actions Interface ─────────────────────────────────────────────────
export interface FamiliarActions {
// Summoning and management
summonFamiliar: (familiarId: string) => void;
setActiveFamiliar: (instanceIndex: number, active: boolean) => void;
setFamiliarNickname: (instanceIndex: number, nickname: string) => void;
// Progression
gainFamiliarXp: (amount: number, source: 'combat' | 'gather' | 'meditate' | 'study' | 'time') => void;
upgradeFamiliarAbility: (instanceIndex: number, abilityType: FamiliarAbilityType) => void;
// Computation
getActiveFamiliarBonuses: () => FamiliarBonuses;
getAvailableFamiliars: () => string[];
}
// ─── Computed Bonuses ───────────────────────────────────────────────────────────
export interface FamiliarBonuses {
damageMultiplier: number;
manaRegenBonus: number;
autoGatherRate: number;
autoConvertRate: number;
critChanceBonus: number;
castSpeedMultiplier: number;
elementalDamageMultiplier: number;
lifeStealPercent: number;
thornsPercent: number;
insightMultiplier: number;
manaShieldAmount: number;
}
export const DEFAULT_FAMILIAR_BONUSES: FamiliarBonuses = {
damageMultiplier: 1,
manaRegenBonus: 0,
autoGatherRate: 0,
autoConvertRate: 0,
critChanceBonus: 0,
castSpeedMultiplier: 1,
elementalDamageMultiplier: 1,
lifeStealPercent: 0,
thornsPercent: 0,
insightMultiplier: 1,
manaShieldAmount: 0,
};
// ─── Familiar Slice Factory ─────────────────────────────────────────────────────
export function createFamiliarSlice(
set: (fn: (state: GameState) => Partial<GameState>) => void,
get: () => GameState
): FamiliarActions {
return {
// Summon a new familiar
summonFamiliar: (familiarId: string) => {
const state = get();
const familiarDef = FAMILIARS_DEF[familiarId];
if (!familiarDef) return;
// Check if already owned
if (state.familiars.some(f => f.familiarId === familiarId)) return;
// Check unlock condition
if (!canUnlockFamiliar(
familiarDef,
state.maxFloorReached,
state.signedPacts,
state.totalManaGathered,
Object.keys(state.skills).length
)) return;
// Create new familiar instance
const newInstance: FamiliarInstance = {
familiarId,
level: 1,
bond: 0,
experience: 0,
abilities: familiarDef.abilities.map(a => ({
type: a.type,
level: 1,
})),
active: false,
};
// Add to familiars list
set((s) => ({
familiars: [...s.familiars, newInstance],
log: [`🌟 ${familiarDef.name} has answered your call!`, ...s.log.slice(0, 49)],
}));
},
// Set a familiar as active/inactive
setActiveFamiliar: (instanceIndex: number, active: boolean) => {
const state = get();
if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return;
const activeCount = state.familiars.filter(f => f.active).length;
// Check if we have slots available
if (active && activeCount >= state.activeFamiliarSlots) {
// Deactivate another familiar first
const newFamiliars = [...state.familiars];
const activeIndex = newFamiliars.findIndex(f => f.active);
if (activeIndex >= 0) {
newFamiliars[activeIndex] = { ...newFamiliars[activeIndex], active: false };
}
newFamiliars[instanceIndex] = { ...newFamiliars[instanceIndex], active };
set({ familiars: newFamiliars });
} else {
// Just toggle the familiar
const newFamiliars = [...state.familiars];
newFamiliars[instanceIndex] = { ...newFamiliars[instanceIndex], active };
set({ familiars: newFamiliars });
}
},
// Set a familiar's nickname
setFamiliarNickname: (instanceIndex: number, nickname: string) => {
const state = get();
if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return;
const newFamiliars = [...state.familiars];
newFamiliars[instanceIndex] = {
...newFamiliars[instanceIndex],
nickname: nickname || undefined
};
set({ familiars: newFamiliars });
},
// Grant XP to all active familiars
gainFamiliarXp: (amount: number, _source: 'combat' | 'gather' | 'meditate' | 'study' | 'time') => {
const state = get();
if (state.familiars.length === 0) return;
const newFamiliars = [...state.familiars];
let leveled = false;
for (let i = 0; i < newFamiliars.length; i++) {
const familiar = newFamiliars[i];
if (!familiar.active) continue;
const def = FAMILIARS_DEF[familiar.familiarId];
if (!def) continue;
// Apply bond multiplier to XP gain
const bondMultiplier = 1 + (familiar.bond / 100);
const xpGain = Math.floor(amount * bondMultiplier);
let newExp = familiar.experience + xpGain;
let newLevel = familiar.level;
// Check for level ups
while (newLevel < 100 && newExp >= getFamiliarXpRequired(newLevel)) {
newExp -= getFamiliarXpRequired(newLevel);
newLevel++;
leveled = true;
}
// Gain bond passively
const newBond = Math.min(100, familiar.bond + 0.01);
newFamiliars[i] = {
...familiar,
level: newLevel,
experience: newExp,
bond: newBond,
};
}
set({
familiars: newFamiliars,
totalFamiliarXpEarned: state.totalFamiliarXpEarned + amount,
...(leveled ? { log: ['📈 Your familiar has grown stronger!', ...state.log.slice(0, 49)] } : {}),
});
},
// Upgrade a familiar's ability
upgradeFamiliarAbility: (instanceIndex: number, abilityType: FamiliarAbilityType) => {
const state = get();
if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return;
const familiar = state.familiars[instanceIndex];
const def = FAMILIARS_DEF[familiar.familiarId];
if (!def) return;
// Find the ability
const abilityIndex = familiar.abilities.findIndex(a => a.type === abilityType);
if (abilityIndex < 0) return;
const ability = familiar.abilities[abilityIndex];
if (ability.level >= 10) return; // Max level
// Cost: level * 100 XP
const cost = ability.level * 100;
if (familiar.experience < cost) return;
// Upgrade
const newAbilities = [...familiar.abilities];
newAbilities[abilityIndex] = { ...ability, level: ability.level + 1 };
const newFamiliars = [...state.familiars];
newFamiliars[instanceIndex] = {
...familiar,
abilities: newAbilities,
experience: familiar.experience - cost,
};
set({ familiars: newFamiliars });
},
// Get total bonuses from active familiars
getActiveFamiliarBonuses: (): FamiliarBonuses => {
const state = get();
const bonuses = { ...DEFAULT_FAMILIAR_BONUSES };
for (const familiar of state.familiars) {
if (!familiar.active) continue;
const def = FAMILIARS_DEF[familiar.familiarId];
if (!def) continue;
// Bond multiplier: up to 50% bonus at max bond
const bondMultiplier = 1 + (familiar.bond / 200);
for (const abilityInst of familiar.abilities) {
const abilityDef = def.abilities.find(a => a.type === abilityInst.type);
if (!abilityDef) continue;
const value = getFamiliarAbilityValue(abilityDef, familiar.level, abilityInst.level) * bondMultiplier;
switch (abilityInst.type) {
case 'damageBonus':
bonuses.damageMultiplier += value / 100;
break;
case 'manaRegen':
bonuses.manaRegenBonus += value;
break;
case 'autoGather':
bonuses.autoGatherRate += value;
break;
case 'autoConvert':
bonuses.autoConvertRate += value;
break;
case 'critChance':
bonuses.critChanceBonus += value;
break;
case 'castSpeed':
bonuses.castSpeedMultiplier += value / 100;
break;
case 'elementalBonus':
bonuses.elementalDamageMultiplier += value / 100;
break;
case 'lifeSteal':
bonuses.lifeStealPercent += value;
break;
case 'thorns':
bonuses.thornsPercent += value;
break;
case 'bonusGold':
bonuses.insightMultiplier += value / 100;
break;
case 'manaShield':
bonuses.manaShieldAmount += value;
break;
}
}
}
return bonuses;
},
// Get list of available (unlocked but not owned) familiars
getAvailableFamiliars: (): string[] => {
const state = get();
const owned = new Set(state.familiars.map(f => f.familiarId));
return Object.values(FAMILIARS_DEF)
.filter(f =>
!owned.has(f.id) &&
canUnlockFamiliar(
f,
state.maxFloorReached,
state.signedPacts,
state.totalManaGathered,
Object.keys(state.skills).length
)
)
.map(f => f.id);
},
};
}
// ─── Familiar Tick Processing ───────────────────────────────────────────────────
// Process familiar-related tick effects (called from main tick)
export function processFamiliarTick(
state: Pick<GameState, 'familiars' | 'rawMana' | 'elements' | 'totalManaGathered' | 'activeFamiliarSlots'>,
familiarBonuses: FamiliarBonuses
): { rawMana: number; elements: GameState['elements']; totalManaGathered: number; gatherLog?: string } {
let rawMana = state.rawMana;
let elements = state.elements;
let totalManaGathered = state.totalManaGathered;
let gatherLog: string | undefined;
// Auto-gather from familiars
if (familiarBonuses.autoGatherRate > 0) {
const gathered = familiarBonuses.autoGatherRate * HOURS_PER_TICK;
rawMana += gathered;
totalManaGathered += gathered;
if (gathered >= 1) {
gatherLog = `✨ Familiars gathered ${Math.floor(gathered)} mana`;
}
}
// Auto-convert from familiars
if (familiarBonuses.autoConvertRate > 0) {
const convertAmount = Math.min(
familiarBonuses.autoConvertRate * HOURS_PER_TICK,
Math.floor(rawMana / 5) // 5 raw mana per element
);
if (convertAmount > 0) {
// Find unlocked elements with space
const unlockedElements = Object.entries(elements)
.filter(([, e]) => e.unlocked && e.current < e.max)
.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
if (unlockedElements.length > 0) {
const [targetId, targetState] = unlockedElements[0];
const canConvert = Math.min(convertAmount, targetState.max - targetState.current);
rawMana -= canConvert * 5;
elements = {
...elements,
[targetId]: { ...targetState, current: targetState.current + canConvert },
};
}
}
}
return { rawMana, elements, totalManaGathered, gatherLog };
}
// Grant starting familiar to new players
export function grantStartingFamiliar(): FamiliarInstance[] {
const starterDef = FAMILIARS_DEF[STARTING_FAMILIAR];
if (!starterDef) return [];
return [{
familiarId: STARTING_FAMILIAR,
level: 1,
bond: 0,
experience: 0,
abilities: starterDef.abilities.map(a => ({
type: a.type,
level: 1,
})),
active: true, // Start with familiar active
}];
}

View File

@@ -39,6 +39,15 @@ import {
} from './crafting-slice';
import { EQUIPMENT_TYPES } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import {
createFamiliarSlice,
processFamiliarTick,
grantStartingFamiliar,
type FamiliarActions,
type FamiliarBonuses,
DEFAULT_FAMILIAR_BONUSES,
} from './familiar-slice';
import { rollLootDrops } from './data/loot-drops';
// Default empty effects for when effects aren't provided
const DEFAULT_EFFECTS: ComputedEffects = {
@@ -528,14 +537,48 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
incursionStrength: 0,
containmentWards: 0,
log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'],
// Combo System
combo: {
count: 0,
multiplier: 1,
lastCastTime: 0,
decayTimer: 0,
maxCombo: 0,
elementChain: [],
},
totalTicks: 0,
// Loot System
lootInventory: {
materials: {},
essence: {},
blueprints: [],
},
lootDropsToday: 0,
// Achievements
achievements: {
unlocked: [],
progress: {},
},
totalDamageDealt: 0,
totalSpellsCast: 0,
totalCraftsCompleted: 0,
// Familiars
familiars: grantStartingFamiliar(),
activeFamiliarSlots: 1,
familiarSummonProgress: 0,
totalFamiliarXpEarned: 0,
log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. A friendly Mana Wisp floats nearby. Gather your strength, mage.'],
loopInsight: 0,
};
}
// ─── Game Store ───────────────────────────────────────────────────────────────
interface GameStore extends GameState, CraftingActions {
interface GameStore extends GameState, CraftingActions, FamiliarActions {
// Actions
tick: () => void;
gatherMana: () => void;
@@ -573,6 +616,7 @@ export const useGameStore = create<GameStore>()(
persist(
(set, get) => ({
...makeInitial(),
...createFamiliarSlice(set, get),
getMaxMana: () => computeMaxMana(get()),
getRegen: () => computeRegen(get()),
@@ -600,8 +644,16 @@ export const useGameStore = create<GameStore>()(
// Compute unified effects (includes skill upgrades AND equipment enchantments)
const effects = getUnifiedEffects(state);
// Compute familiar bonuses
const familiarBonuses = state.familiars.length > 0
? (() => {
const slice = createFamiliarSlice(set, get);
return slice.getActiveFamiliarBonuses();
})()
: DEFAULT_FAMILIAR_BONUSES;
const maxMana = computeMaxMana(state, effects);
const baseRegen = computeRegen(state, effects);
const baseRegen = computeRegen(state, effects) + familiarBonuses.manaRegenBonus;
// Time progression
let hour = state.hour + HOURS_PER_TICK;
@@ -653,18 +705,29 @@ export const useGameStore = create<GameStore>()(
// Calculate effective regen with incursion and meditation
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
// Mana regeneration
let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
let totalManaGathered = state.totalManaGathered;
// Familiar auto-gather and auto-convert
let elements = state.elements;
if (familiarBonuses.autoGatherRate > 0 || familiarBonuses.autoConvertRate > 0) {
const familiarUpdates = processFamiliarTick(
{ rawMana, elements, totalManaGathered, familiars: state.familiars, activeFamiliarSlots: state.activeFamiliarSlots },
familiarBonuses
);
rawMana = Math.min(familiarUpdates.rawMana, maxMana);
elements = familiarUpdates.elements;
totalManaGathered = familiarUpdates.totalManaGathered;
}
// Study progress
let currentStudyTarget = state.currentStudyTarget;
let skills = state.skills;
let skillProgress = state.skillProgress;
let spells = state.spells;
let log = state.log;
let elements = state.elements;
let unlockedEffects = state.unlockedEffects;
if (state.currentAction === 'study' && currentStudyTarget) {
@@ -738,8 +801,27 @@ export const useGameStore = create<GameStore>()(
}
// Combat - MULTI-SPELL casting from all equipped weapons
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates } = state;
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates, combo, totalTicks, lootInventory, achievements, totalDamageDealt, totalSpellsCast } = state;
const floorElement = getFloorElement(currentFloor);
// Increment total ticks
const newTotalTicks = totalTicks + 1;
// Combo decay - decay combo when not climbing or when decay timer expires
let newCombo = { ...combo };
if (state.currentAction !== 'climb') {
// Rapidly decay combo when not climbing
newCombo.count = Math.max(0, newCombo.count - 5);
newCombo.multiplier = 1 + newCombo.count * 0.02;
} else if (newCombo.count > 0) {
// Slow decay while climbing but not casting
newCombo.decayTimer--;
if (newCombo.decayTimer <= 0) {
newCombo.count = Math.max(0, newCombo.count - 2);
newCombo.multiplier = 1 + newCombo.count * 0.02;
newCombo.decayTimer = 10;
}
}
if (state.currentAction === 'climb') {
// Get all spells from equipped caster weapons
@@ -768,7 +850,7 @@ export const useGameStore = create<GameStore>()(
// Compute attack speed from quickCast skill and upgrades
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier * familiarBonuses.castSpeedMultiplier;
// Process each active spell
for (const { spellId, equipmentId } of activeSpells) {
@@ -796,9 +878,41 @@ export const useGameStore = create<GameStore>()(
elements = afterCost.elements;
totalManaGathered += spellDef.cost.amount;
// Increment spell cast counter
totalSpellsCast++;
// ─── Combo System ───
// Build combo on each cast
newCombo.count = Math.min(100, newCombo.count + 1);
newCombo.lastCastTime = newTotalTicks;
newCombo.decayTimer = 10; // Reset decay timer
newCombo.maxCombo = Math.max(newCombo.maxCombo, newCombo.count);
// Track element chain
const spellElement = spellDef.elem;
newCombo.elementChain = [...newCombo.elementChain.slice(-2), spellElement];
// Calculate combo multiplier
let comboMult = 1 + newCombo.count * 0.02; // +2% per combo
// Element chain bonus: +25% if last 3 spells were different elements
const uniqueElements = new Set(newCombo.elementChain);
if (newCombo.elementChain.length === 3 && uniqueElements.size === 3) {
comboMult += 0.25;
// Log elemental chain occasionally
if (newCombo.count % 10 === 0) {
log = [`🌈 Elemental Chain! (${newCombo.elementChain.join(' → ')})`, ...log.slice(0, 49)];
}
}
newCombo.multiplier = Math.min(3.0, comboMult);
// Calculate damage
let dmg = calcDamage(state, spellId, floorElement);
// Apply combo multiplier FIRST
dmg *= newCombo.multiplier;
// Apply upgrade damage multipliers and bonuses
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
@@ -811,7 +925,16 @@ export const useGameStore = create<GameStore>()(
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
dmg *= 1.5;
}
// Familiar bonuses
dmg *= familiarBonuses.damageMultiplier;
dmg *= familiarBonuses.elementalDamageMultiplier;
// Familiar crit chance bonus
if (Math.random() < familiarBonuses.critChanceBonus / 100) {
dmg *= 1.5;
}
// Spell echo - chance to cast again
const echoChance = (skills.spellEcho || 0) * 0.1;
if (Math.random() < echoChance) {
@@ -825,6 +948,15 @@ export const useGameStore = create<GameStore>()(
const healAmount = dmg * lifestealEffect.value;
rawMana = Math.min(rawMana + healAmount, maxMana);
}
// Familiar lifesteal
if (familiarBonuses.lifeStealPercent > 0) {
const healAmount = dmg * (familiarBonuses.lifeStealPercent / 100);
rawMana = Math.min(rawMana + healAmount, maxMana);
}
// Track total damage for achievements
totalDamageDealt += dmg;
// Apply damage
floorHP = Math.max(0, floorHP - dmg);
@@ -835,6 +967,33 @@ export const useGameStore = create<GameStore>()(
if (floorHP <= 0) {
// Floor cleared
const wasGuardian = GUARDIANS[currentFloor];
// ─── Loot Drop System ───
const lootDrops = rollLootDrops(currentFloor, !!wasGuardian, 0);
for (const { drop, amount } of lootDrops) {
if (drop.type === 'material') {
lootInventory.materials[drop.id] = (lootInventory.materials[drop.id] || 0) + amount;
log = [`💎 Found: ${drop.name}!`, ...log.slice(0, 49)];
} else if (drop.type === 'essence' && drop.id) {
// Extract element from essence drop id (e.g., 'fireEssenceDrop' -> 'fire')
const element = drop.id.replace('EssenceDrop', '');
if (elements[element]) {
const gain = Math.min(amount, elements[element].max - elements[element].current);
elements[element] = { ...elements[element], current: elements[element].current + gain };
log = [`✨ Gained ${gain} ${element} essence!`, ...log.slice(0, 49)];
}
} else if (drop.type === 'gold') {
rawMana += amount;
log = [`💫 Gained ${amount} mana from ${drop.name}!`, ...log.slice(0, 49)];
} else if (drop.type === 'blueprint') {
if (!lootInventory.blueprints.includes(drop.id)) {
lootInventory.blueprints.push(drop.id);
log = [`📜 Discovered: ${drop.name}!`, ...log.slice(0, 49)];
}
}
}
if (wasGuardian && !signedPacts.includes(currentFloor)) {
signedPacts = [...signedPacts, currentFloor];
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
@@ -852,6 +1011,11 @@ export const useGameStore = create<GameStore>()(
floorHP = floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
// Reset combo on floor change (partial reset - keep 50%)
newCombo.count = Math.floor(newCombo.count * 0.5);
newCombo.multiplier = 1 + newCombo.count * 0.02;
newCombo.elementChain = [];
// Reset ALL spell progress on floor change
equipmentSpellStates = equipmentSpellStates.map(s => ({ ...s, castProgress: 0 }));
spellState = { ...spellState, castProgress: 0 };
@@ -865,6 +1029,9 @@ export const useGameStore = create<GameStore>()(
);
}
}
// Update combo state
combo = newCombo;
// Process crafting actions (design, prepare, enchant)
const craftingUpdates = processCraftingTick(
@@ -912,11 +1079,47 @@ export const useGameStore = create<GameStore>()(
spells,
elements,
log,
equipmentSpellStates,
equipmentSpellStates,
combo,
totalTicks: newTotalTicks,
lootInventory,
achievements,
totalDamageDealt,
totalSpellsCast,
});
return;
}
// Grant XP to active familiars based on activity
let familiars = state.familiars;
if (familiars.some(f => f.active)) {
let xpGain = 0;
let xpSource: 'combat' | 'gather' | 'meditate' | 'study' | 'time' = 'time';
if (state.currentAction === 'climb') {
xpGain = 2 * HOURS_PER_TICK; // 2 XP per hour in combat
xpSource = 'combat';
} else if (state.currentAction === 'meditate') {
xpGain = 1 * HOURS_PER_TICK;
xpSource = 'meditate';
} else if (state.currentAction === 'study') {
xpGain = 1.5 * HOURS_PER_TICK;
xpSource = 'study';
} else {
xpGain = 0.5 * HOURS_PER_TICK; // Passive XP
}
// Update familiar XP and bond
familiars = familiars.map(f => {
if (!f.active) return f;
const bondMultiplier = 1 + (f.bond / 100);
const xpGained = Math.floor(xpGain * bondMultiplier);
const newXp = f.experience + xpGained;
const newBond = Math.min(100, f.bond + 0.02); // Slow bond gain
return { ...f, experience: newXp, bond: newBond };
});
}
set({
day,
hour,
@@ -937,6 +1140,13 @@ export const useGameStore = create<GameStore>()(
unlockedEffects,
log,
equipmentSpellStates,
combo,
totalTicks: newTotalTicks,
lootInventory,
achievements,
totalDamageDealt,
totalSpellsCast,
familiars,
...craftingUpdates,
});
},

View File

@@ -213,6 +213,85 @@ export interface EquipmentSpellState {
castProgress: number; // 0-1 progress toward next cast
}
// ─── Combo System ─────────────────────────────────────────────────────────────
export interface ComboState {
count: number; // Number of consecutive spell casts
multiplier: number; // Current damage multiplier (1.0 = base)
lastCastTime: number; // Game tick when last spell was cast
decayTimer: number; // Ticks until combo decays
maxCombo: number; // Highest combo achieved this loop
elementChain: string[]; // Last 3 elements cast (for elemental chain bonus)
}
export const COMBO_CONFIG = {
maxCombo: 100, // Maximum combo count
baseDecayTicks: 10, // Ticks before combo starts decaying
decayRate: 2, // Combo lost per decay tick
baseMultiplier: 0.02, // +2% damage per combo
maxMultiplier: 3.0, // 300% max multiplier
elementalChainBonus: 0.25, // +25% for 3 different elements
perfectChainBonus: 0.5, // +50% for perfect element wheel
} as const;
// ─── Loot Drop System ─────────────────────────────────────────────────────────
export interface LootDrop {
id: string;
name: string;
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
type: 'equipment' | 'material' | 'gold' | 'essence' | 'blueprint';
minFloor: number; // Minimum floor for this drop
dropChance: number; // Base drop chance (0-1)
guardianOnly?: boolean; // Only drops from guardians
effect?: LootEffect;
amount?: { min: number; max: number }; // For gold/essence
}
export interface LootEffect {
type: 'manaBonus' | 'damageBonus' | 'regenBonus' | 'castSpeed' | 'critChance' | 'special';
value: number;
desc: string;
}
export interface LootInventory {
materials: Record<string, number>; // materialId -> count
essence: Record<string, number>; // element -> count
blueprints: string[]; // unlocked blueprint IDs
}
// ─── Achievement System ───────────────────────────────────────────────────────
export interface AchievementDef {
id: string;
name: string;
desc: string;
category: 'combat' | 'progression' | 'crafting' | 'magic' | 'special';
requirement: AchievementRequirement;
reward: AchievementReward;
hidden?: boolean; // Hidden until unlocked
}
export interface AchievementRequirement {
type: 'floor' | 'combo' | 'spells' | 'damage' | 'mana' | 'craft' | 'pact' | 'time';
value: number;
subType?: string; // e.g., specific element, spell type
}
export interface AchievementReward {
insight?: number;
manaBonus?: number;
regenBonus?: number;
damageBonus?: number;
unlockEffect?: string; // Unlocks an enchantment effect
title?: string; // Display title
}
export interface AchievementState {
unlocked: string[]; // Achievement IDs
progress: Record<string, number>; // achievementId -> progress
}
export interface BlueprintDef {
id: string;
name: string;
@@ -226,6 +305,69 @@ export interface BlueprintDef {
learned: boolean;
}
// ─── Familiar System ───────────────────────────────────────────────────────────
// Familiar role determines their primary function
export type FamiliarRole = 'combat' | 'mana' | 'support' | 'guardian';
// Familiar ability types
export type FamiliarAbilityType =
| 'damageBonus' // +X% damage
| 'manaRegen' // +X mana regen
| 'autoGather' // Gathers mana automatically
| 'critChance' // +X% crit chance
| 'castSpeed' // +X% cast speed
| 'manaShield' // Absorbs damage, costs mana
| 'elementalBonus' // +X% elemental damage
| 'lifeSteal' // Heal on hit
| 'bonusGold' // +X% insight gain
| 'autoConvert' // Auto-converts mana to elements
| 'thorns'; // Reflects damage
export interface FamiliarAbility {
type: FamiliarAbilityType;
baseValue: number; // Base effect value
scalingPerLevel: number; // How much it increases per familiar level
desc: string;
}
// Familiar definition (static data)
export interface FamiliarDef {
id: string;
name: string;
desc: string;
role: FamiliarRole;
element: string; // Associated element
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
abilities: FamiliarAbility[];
baseStats: {
power: number; // Affects ability strength
bond: number; // How fast bond grows
};
unlockCondition?: {
type: 'floor' | 'pact' | 'mana' | 'study';
value: number;
};
flavorText?: string;
}
// Familiar instance (player's owned familiar)
export interface FamiliarInstance {
familiarId: string; // Reference to FamiliarDef
level: number; // 1-100
bond: number; // 0-100, affects power multiplier
experience: number; // XP towards next level
abilities: Array<{
type: FamiliarAbilityType;
level: number; // Ability level (1-10)
}>;
active: boolean; // Is this familiar currently summoned?
nickname?: string; // Optional custom name
}
// Familiar experience gain sources
export type FamiliarXpSource = 'combat' | 'gather' | 'meditate' | 'study' | 'time';
export type GameAction = 'meditate' | 'climb' | 'study' | 'craft' | 'repair' | 'convert' | 'design' | 'prepare' | 'enchant';
export interface ScheduleBlock {
@@ -326,6 +468,26 @@ export interface GameState {
incursionStrength: number;
containmentWards: number;
// Combo System
combo: ComboState;
totalTicks: number; // Total ticks this loop (for combo timing)
// Loot System
lootInventory: LootInventory;
lootDropsToday: number; // Track drops for diminishing returns
// Achievements
achievements: AchievementState;
totalDamageDealt: number; // For damage achievements
totalSpellsCast: number; // For spell achievements
totalCraftsCompleted: number; // For craft achievements
// Familiars
familiars: FamiliarInstance[]; // Owned familiars
activeFamiliarSlots: number; // How many familiars can be active (default 1)
familiarSummonProgress: number; // Progress toward summoning new familiar (0-100)
totalFamiliarXpEarned: number; // Lifetime XP earned for familiars
// Log
log: string[];