feat: guardian defensive stats — shield, barrier, health regen + stat label renames
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-29T13:42:14.414Z
|
Generated: 2026-05-29T15:18:18.868Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-29T13:42:12.691Z",
|
"generated": "2026-05-29T15:18:17.066Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
@@ -408,11 +408,13 @@
|
|||||||
"data/golems/types.ts"
|
"data/golems/types.ts"
|
||||||
],
|
],
|
||||||
"data/guardian-data.ts": [
|
"data/guardian-data.ts": [
|
||||||
"types.ts"
|
"types.ts",
|
||||||
|
"utils/guardian-utils.ts"
|
||||||
],
|
],
|
||||||
"data/guardian-encounters.ts": [
|
"data/guardian-encounters.ts": [
|
||||||
"data/guardian-data.ts",
|
"data/guardian-data.ts",
|
||||||
"types.ts"
|
"types.ts",
|
||||||
|
"utils/guardian-utils.ts"
|
||||||
],
|
],
|
||||||
"data/loot-drops.ts": [
|
"data/loot-drops.ts": [
|
||||||
"types/game.ts"
|
"types/game.ts"
|
||||||
@@ -702,6 +704,9 @@
|
|||||||
"data/guardian-encounters.ts"
|
"data/guardian-encounters.ts"
|
||||||
],
|
],
|
||||||
"utils/formatting.ts": [],
|
"utils/formatting.ts": [],
|
||||||
|
"utils/guardian-utils.ts": [
|
||||||
|
"constants/elements.ts"
|
||||||
|
],
|
||||||
"utils/index.ts": [
|
"utils/index.ts": [
|
||||||
"utils/combat-utils.ts",
|
"utils/combat-utils.ts",
|
||||||
"utils/floor-utils.ts",
|
"utils/floor-utils.ts",
|
||||||
|
|||||||
@@ -180,12 +180,27 @@ function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: nu
|
|||||||
>
|
>
|
||||||
{nextGuardianData.element.join(' + ')}
|
{nextGuardianData.element.join(' + ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-xs text-gray-500">HP: {fmt(nextGuardianData.hp)}</span>
|
<span className="text-xs text-gray-500">Health: {fmt(nextGuardianData.hp)}</span>
|
||||||
{nextGuardianData.armor && (
|
{nextGuardianData.armor && (
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{nextGuardianData.shield && nextGuardianData.shield > 0 && (
|
||||||
|
<span className="text-xs text-cyan-400">
|
||||||
|
Shield: {fmt(nextGuardianData.shield)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{nextGuardianData.barrier && nextGuardianData.barrier > 0 && (
|
||||||
|
<span className="text-xs text-blue-400">
|
||||||
|
Barrier: {Math.round(nextGuardianData.barrier * 100)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{nextGuardianData.healthRegen && nextGuardianData.healthRegen > 0 && (
|
||||||
|
<span className="text-xs text-green-400">
|
||||||
|
Regen: {nextGuardianData.healthRegenIsPercent ? nextGuardianData.healthRegen + '%/tick' : nextGuardianData.healthRegen + '/tick'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +304,7 @@ function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; gu
|
|||||||
>
|
>
|
||||||
{guardian.element.join(' + ')}
|
{guardian.element.join(' + ')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-gray-500">HP: {fmt(guardian.hp)}</span>
|
<span className="text-[10px] text-gray-500">Health: {fmt(guardian.hp)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ELEMENTS } from '@/lib/game/constants';
|
|||||||
import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
|
import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react';
|
import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight, Heart, Hexagon } from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
|
|
||||||
@@ -152,20 +152,48 @@ GuardianCard.displayName = 'GuardianCard';
|
|||||||
// ─── Guardian Stats ──────────────────────────────────────────────────────────
|
// ─── Guardian Stats ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function GuardianStats({ guardian }: { guardian: GuardianDef }) {
|
function GuardianStats({ guardian }: { guardian: GuardianDef }) {
|
||||||
|
const hasShield = !!(guardian.shield && guardian.shield > 0);
|
||||||
|
const hasBarrier = !!(guardian.barrier && guardian.barrier > 0);
|
||||||
|
const hasHealthRegen = !!(guardian.healthRegen && guardian.healthRegen > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center gap-1 text-gray-400">
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
<Shield className="w-3 h-3" />
|
<div className="flex items-center gap-1 text-gray-400">
|
||||||
<span>HP: {guardian.hp.toLocaleString()}</span>
|
<Shield className="w-3 h-3" />
|
||||||
</div>
|
<span>Health: {guardian.hp.toLocaleString()}</span>
|
||||||
<div className="flex items-center gap-1 text-gray-400">
|
</div>
|
||||||
<Swords className="w-3 h-3" />
|
<div className="flex items-center gap-1 text-gray-400">
|
||||||
<span>PWR: {guardian.power.toLocaleString()}</span>
|
<Swords className="w-3 h-3" />
|
||||||
</div>
|
<span>Power: {guardian.power.toLocaleString()}</span>
|
||||||
<div className="flex items-center gap-1 text-gray-400">
|
</div>
|
||||||
<Shield className="w-3 h-3" />
|
<div className="flex items-center gap-1 text-gray-400">
|
||||||
<span>ARM: {Math.round((guardian.armor ?? 0) * 100)}%</span>
|
<Shield className="w-3 h-3" />
|
||||||
|
<span>Armor: {Math.round((guardian.armor ?? 0) * 100)}%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{(hasShield || hasBarrier || hasHealthRegen) && (
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs border-t border-gray-700/40 pt-1.5">
|
||||||
|
{hasShield && (
|
||||||
|
<div className="flex items-center gap-1 text-cyan-400">
|
||||||
|
<Hexagon className="w-3 h-3" />
|
||||||
|
<span>Shield: {guardian.shield!.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasBarrier && (
|
||||||
|
<div className="flex items-center gap-1 text-blue-400">
|
||||||
|
<Shield className="w-3 h-3" />
|
||||||
|
<span>Barrier: {Math.round(guardian.barrier! * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasHealthRegen && (
|
||||||
|
<div className="flex items-center gap-1 text-green-400">
|
||||||
|
<Heart className="w-3 h-3" />
|
||||||
|
<span>Regen: {guardian.healthRegenIsPercent ? guardian.healthRegen + '%/tick' : guardian.healthRegen + '/tick'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ function hp(floor: number): number {
|
|||||||
return Math.floor(base * Math.pow(floor / 10, exponent));
|
return Math.floor(base * Math.pow(floor / 10, exponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pactCost(hpVal: number, power: number, armor: number): number {
|
function pactCost(hpVal: number, power: number, armor: number, shield: number, barrier: number): number {
|
||||||
return Math.floor(hpVal * 0.3 + power * 5 + hpVal * armor * 0.5);
|
return Math.floor(hpVal * 0.3 + power * 5 + hpVal * armor * 0.5 + shield * 2 + hpVal * barrier * 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mk(
|
function mk(
|
||||||
@@ -35,11 +35,21 @@ function mk(
|
|||||||
boons: GuardianDef['boons'],
|
boons: GuardianDef['boons'],
|
||||||
uniquePerk: string,
|
uniquePerk: string,
|
||||||
effects: GuardianDef['effects'],
|
effects: GuardianDef['effects'],
|
||||||
|
defensive?: {
|
||||||
|
shield?: number;
|
||||||
|
shieldRegen?: number;
|
||||||
|
barrier?: number;
|
||||||
|
barrierRegen?: number;
|
||||||
|
healthRegen?: number;
|
||||||
|
healthRegenIsPercent?: boolean;
|
||||||
|
},
|
||||||
): GuardianDef {
|
): GuardianDef {
|
||||||
const hpVal = hp(floor);
|
const hpVal = hp(floor);
|
||||||
const power = Math.floor(hpVal * 0.5);
|
const power = Math.floor(hpVal * 0.5);
|
||||||
const arm = armor;
|
const arm = armor;
|
||||||
const pc = pactCost(hpVal, power, arm);
|
const shield = defensive?.shield ?? 0;
|
||||||
|
const barrier = defensive?.barrier ?? 0;
|
||||||
|
const pc = pactCost(hpVal, power, arm, shield, barrier);
|
||||||
const pt = 2 + Math.floor(floor / 10);
|
const pt = 2 + Math.floor(floor / 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -49,6 +59,12 @@ function mk(
|
|||||||
pact: pactMult,
|
pact: pactMult,
|
||||||
color,
|
color,
|
||||||
armor: arm,
|
armor: arm,
|
||||||
|
shield,
|
||||||
|
shieldRegen: defensive?.shieldRegen ?? 0,
|
||||||
|
barrier,
|
||||||
|
barrierRegen: defensive?.barrierRegen ?? 0,
|
||||||
|
healthRegen: defensive?.healthRegen ?? 0,
|
||||||
|
healthRegenIsPercent: defensive?.healthRegenIsPercent ?? false,
|
||||||
boons,
|
boons,
|
||||||
pactCost: pc,
|
pactCost: pc,
|
||||||
pactTime: pt,
|
pactTime: pt,
|
||||||
@@ -82,6 +98,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Water spells deal +15% damage',
|
'Water spells deal +15% damage',
|
||||||
[{ type: 'armor_pierce', value: 0.15 }],
|
[{ type: 'armor_pierce', value: 0.15 }],
|
||||||
|
{ healthRegen: 10, healthRegenIsPercent: false },
|
||||||
),
|
),
|
||||||
30: mk(30, 'Ventus Rex', ['air'], '#00D4FF', 0.18, 2.0,
|
30: mk(30, 'Ventus Rex', ['air'], '#00D4FF', 0.18, 2.0,
|
||||||
[
|
[
|
||||||
@@ -98,6 +115,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Earth spells deal +25% damage to guardians',
|
'Earth spells deal +25% damage to guardians',
|
||||||
[{ type: 'armor_pierce', value: 0.2 }],
|
[{ type: 'armor_pierce', value: 0.2 }],
|
||||||
|
{ shield: 200, shieldRegen: 5 },
|
||||||
),
|
),
|
||||||
50: mk(50, 'Lux Aeterna', ['light'], '#FFD700', 0.20, 2.5,
|
50: mk(50, 'Lux Aeterna', ['light'], '#FFD700', 0.20, 2.5,
|
||||||
[
|
[
|
||||||
@@ -106,6 +124,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Light spells reveal enemy weaknesses (+20% damage)',
|
'Light spells reveal enemy weaknesses (+20% damage)',
|
||||||
[{ type: 'crit_chance', value: 0.1 }],
|
[{ type: 'crit_chance', value: 0.1 }],
|
||||||
|
{ barrier: 0.05, barrierRegen: 0.01 },
|
||||||
),
|
),
|
||||||
60: mk(60, 'Umbra Mortis', ['dark'], '#9B59B6', 0.22, 2.75,
|
60: mk(60, 'Umbra Mortis', ['dark'], '#9B59B6', 0.22, 2.75,
|
||||||
[
|
[
|
||||||
@@ -114,6 +133,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Dark spells deal +25% damage to armored enemies',
|
'Dark spells deal +25% damage to armored enemies',
|
||||||
[{ type: 'crit_damage', value: 0.15 }],
|
[{ type: 'crit_damage', value: 0.15 }],
|
||||||
|
{ healthRegen: 5, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
70: mk(70, 'Mors Ultima', ['death'], '#778CA3', 0.25, 3.0,
|
70: mk(70, 'Mors Ultima', ['death'], '#778CA3', 0.25, 3.0,
|
||||||
[
|
[
|
||||||
@@ -122,6 +142,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Death spells execute enemies below 20% HP',
|
'Death spells execute enemies below 20% HP',
|
||||||
[{ type: 'raw_damage', value: 0.1 }],
|
[{ type: 'raw_damage', value: 0.1 }],
|
||||||
|
{ shield: 400, shieldRegen: 10 },
|
||||||
),
|
),
|
||||||
80: mk(80, 'Vinculum Arcana', ['transference'], '#1ABC9C', 0.20, 3.25,
|
80: mk(80, 'Vinculum Arcana', ['transference'], '#1ABC9C', 0.20, 3.25,
|
||||||
[
|
[
|
||||||
@@ -130,6 +151,7 @@ const TIER1: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Transference spells have 25% reduced cost',
|
'Transference spells have 25% reduced cost',
|
||||||
[{ type: 'cost_reduction', value: 0.25 }],
|
[{ type: 'cost_reduction', value: 0.25 }],
|
||||||
|
{ barrier: 0.08, barrierRegen: 0.02, healthRegen: 3, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,6 +167,7 @@ const TIER2: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Metal spells pierce 20% armor',
|
'Metal spells pierce 20% armor',
|
||||||
[{ type: 'armor_pierce', value: 0.2 }],
|
[{ type: 'armor_pierce', value: 0.2 }],
|
||||||
|
{ shield: 600, shieldRegen: 15, healthRegen: 4, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
100: mk(100, '', ['sand'], '#D4AC0D', 0.25, 3.75,
|
100: mk(100, '', ['sand'], '#D4AC0D', 0.25, 3.75,
|
||||||
[
|
[
|
||||||
@@ -153,6 +176,7 @@ const TIER2: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Sand spells slow enemies by 25%',
|
'Sand spells slow enemies by 25%',
|
||||||
[{ type: 'slow', value: 0.25 }],
|
[{ type: 'slow', value: 0.25 }],
|
||||||
|
{ barrier: 0.10, barrierRegen: 0.03, healthRegen: 5, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0,
|
110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0,
|
||||||
[
|
[
|
||||||
@@ -161,6 +185,7 @@ const TIER2: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Lightning spells chain to 2 additional targets',
|
'Lightning spells chain to 2 additional targets',
|
||||||
[{ type: 'chain', value: 2 }],
|
[{ type: 'chain', value: 2 }],
|
||||||
|
{ shield: 800, shieldRegen: 20, barrier: 0.05, barrierRegen: 0.01 },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,6 +202,7 @@ const TIER3: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Tri-aspect: Metal, Fire, and Earth spells gain +10% effectiveness',
|
'Tri-aspect: Metal, Fire, and Earth spells gain +10% effectiveness',
|
||||||
[{ type: 'armor_pierce', value: 0.25 }, { type: 'burn', value: 0.1 }],
|
[{ type: 'armor_pierce', value: 0.25 }, { type: 'burn', value: 0.1 }],
|
||||||
|
{ shield: 1000, shieldRegen: 25, barrier: 0.05, barrierRegen: 0.01 },
|
||||||
),
|
),
|
||||||
140: mk(140, '', ['sand', 'earth', 'water'], '#C9B896', 0.30, 4.75,
|
140: mk(140, '', ['sand', 'earth', 'water'], '#C9B896', 0.30, 4.75,
|
||||||
[
|
[
|
||||||
@@ -186,6 +212,7 @@ const TIER3: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Tri-aspect: Sand, Earth, and Water spells gain +10% effectiveness',
|
'Tri-aspect: Sand, Earth, and Water spells gain +10% effectiveness',
|
||||||
[{ type: 'slow', value: 0.3 }, { type: 'armor_pierce', value: 0.15 }],
|
[{ type: 'slow', value: 0.3 }, { type: 'armor_pierce', value: 0.15 }],
|
||||||
|
{ barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
150: mk(150, '', ['lightning', 'fire', 'air'], '#FFE066', 0.28, 5.0,
|
150: mk(150, '', ['lightning', 'fire', 'air'], '#FFE066', 0.28, 5.0,
|
||||||
[
|
[
|
||||||
@@ -195,6 +222,7 @@ const TIER3: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Tri-aspect: Lightning, Fire, and Air spells gain +10% effectiveness',
|
'Tri-aspect: Lightning, Fire, and Air spells gain +10% effectiveness',
|
||||||
[{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }],
|
[{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }],
|
||||||
|
{ shield: 1200, shieldRegen: 30, healthRegen: 5, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
160: mk(160, '', ['metal', 'lightning', 'fire', 'earth', 'air'], '#E8C872', 0.35, 5.25,
|
160: mk(160, '', ['metal', 'lightning', 'fire', 'earth', 'air'], '#E8C872', 0.35, 5.25,
|
||||||
[
|
[
|
||||||
@@ -204,6 +232,7 @@ const TIER3: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Fused aspects: Lightning spells gain +20% armor pierce; Metal spells chain once',
|
'Fused aspects: Lightning spells gain +20% armor pierce; Metal spells chain once',
|
||||||
[{ type: 'armor_pierce', value: 0.3 }, { type: 'chain', value: 1 }],
|
[{ type: 'armor_pierce', value: 0.3 }, { type: 'chain', value: 1 }],
|
||||||
|
{ shield: 1500, shieldRegen: 40, barrier: 0.08, barrierRegen: 0.02, healthRegen: 7, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,6 +249,7 @@ const TIER4: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Crystal spells reflect 15% damage back to attackers',
|
'Crystal spells reflect 15% damage back to attackers',
|
||||||
[{ type: 'reflect', value: 0.15 }],
|
[{ type: 'reflect', value: 0.15 }],
|
||||||
|
{ shield: 2000, shieldRegen: 50, barrier: 0.10, barrierRegen: 0.03 },
|
||||||
),
|
),
|
||||||
180: mk(180, '', ['stellar'], '#F0E68C', 0.30, 6.0,
|
180: mk(180, '', ['stellar'], '#F0E68C', 0.30, 6.0,
|
||||||
[
|
[
|
||||||
@@ -228,6 +258,7 @@ const TIER4: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Stellar spells deal +30% damage at night',
|
'Stellar spells deal +30% damage at night',
|
||||||
[{ type: 'night_bonus', value: 0.3 }],
|
[{ type: 'night_bonus', value: 0.3 }],
|
||||||
|
{ barrier: 0.15, barrierRegen: 0.04, healthRegen: 8, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
190: mk(190, '', ['void'], '#4A235A', 0.35, 6.5,
|
190: mk(190, '', ['void'], '#4A235A', 0.35, 6.5,
|
||||||
[
|
[
|
||||||
@@ -237,6 +268,7 @@ const TIER4: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Void spells ignore 40% of all resistances',
|
'Void spells ignore 40% of all resistances',
|
||||||
[{ type: 'resist_ignore', value: 0.4 }],
|
[{ type: 'resist_ignore', value: 0.4 }],
|
||||||
|
{ shield: 2500, shieldRegen: 60, barrier: 0.10, barrierRegen: 0.02, healthRegen: 6, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
200: mk(200, '', ['crystal', 'stellar', 'void'], '#B39DDB', 0.40, 7.0,
|
200: mk(200, '', ['crystal', 'stellar', 'void'], '#B39DDB', 0.40, 7.0,
|
||||||
[
|
[
|
||||||
@@ -246,6 +278,7 @@ const TIER4: Record<number, GuardianDef> = {
|
|||||||
],
|
],
|
||||||
'Exotic convergence: All exotic spells gain +15% effectiveness',
|
'Exotic convergence: All exotic spells gain +15% effectiveness',
|
||||||
[{ type: 'reflect', value: 0.1 }, { type: 'resist_ignore', value: 0.1 }],
|
[{ type: 'reflect', value: 0.1 }, { type: 'resist_ignore', value: 0.1 }],
|
||||||
|
{ shield: 3000, shieldRegen: 80, barrier: 0.12, barrierRegen: 0.03, healthRegen: 10, healthRegenIsPercent: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ export interface CombatState {
|
|||||||
comboHitCount: number;
|
comboHitCount: number;
|
||||||
floorHitCount: number;
|
floorHitCount: number;
|
||||||
|
|
||||||
|
// Guardian defensive state (shield, barrier, regen)
|
||||||
|
guardianShield: number;
|
||||||
|
guardianShieldMax: number;
|
||||||
|
guardianBarrier: number;
|
||||||
|
guardianBarrierMax: number;
|
||||||
|
|
||||||
// Spells
|
// Spells
|
||||||
spells: Record<string, SpellState>;
|
spells: Record<string, SpellState>;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { generateFloorState } from '../utils/room-utils';
|
|||||||
import { generateSpireFloorState } from '../utils/spire-utils';
|
import { generateSpireFloorState } from '../utils/spire-utils';
|
||||||
import { addActivityLogEntry } from '../utils/activity-log';
|
import { addActivityLogEntry } from '../utils/activity-log';
|
||||||
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
||||||
|
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||||
import type { CombatStore } from './combat-state.types';
|
import type { CombatStore } from './combat-state.types';
|
||||||
|
|
||||||
export const useCombatStore = create<CombatStore>()(
|
export const useCombatStore = create<CombatStore>()(
|
||||||
@@ -46,6 +47,12 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
comboHitCount: 0,
|
comboHitCount: 0,
|
||||||
floorHitCount: 0,
|
floorHitCount: 0,
|
||||||
|
|
||||||
|
// Guardian defensive state
|
||||||
|
guardianShield: 0,
|
||||||
|
guardianShieldMax: 0,
|
||||||
|
guardianBarrier: 0,
|
||||||
|
guardianBarrierMax: 0,
|
||||||
|
|
||||||
// Spells
|
// Spells
|
||||||
spells: {
|
spells: {
|
||||||
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
||||||
@@ -253,6 +260,22 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 }));
|
set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetGuardianDefensiveState: () => {
|
||||||
|
set({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
||||||
|
},
|
||||||
|
|
||||||
|
initGuardianDefensiveState: () => {
|
||||||
|
const state = get();
|
||||||
|
const guardian = getGuardianForFloor(state.currentFloor);
|
||||||
|
if (!guardian) return;
|
||||||
|
set({
|
||||||
|
guardianShield: guardian.shield ?? 0,
|
||||||
|
guardianShieldMax: guardian.shield ?? 0,
|
||||||
|
guardianBarrier: guardian.barrier ?? 0,
|
||||||
|
guardianBarrierMax: guardian.barrier ?? 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
processCombatTick: (
|
processCombatTick: (
|
||||||
rawMana: number,
|
rawMana: number,
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
||||||
@@ -316,6 +339,10 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
totalSpellsCast: state.totalSpellsCast,
|
totalSpellsCast: state.totalSpellsCast,
|
||||||
totalDamageDealt: state.totalDamageDealt,
|
totalDamageDealt: state.totalDamageDealt,
|
||||||
totalCraftsCompleted: state.totalCraftsCompleted,
|
totalCraftsCompleted: state.totalCraftsCompleted,
|
||||||
|
guardianShield: state.guardianShield,
|
||||||
|
guardianShieldMax: state.guardianShieldMax,
|
||||||
|
guardianBarrier: state.guardianBarrier,
|
||||||
|
guardianBarrierMax: state.guardianBarrierMax,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// ─── Game Store (Coordinator) ──────────────────────────────────────
|
// Game Store — coordinator, tick pipeline, time/incursion
|
||||||
// Manages: day, hour, incursionStrength, containmentWards
|
|
||||||
// Orchestrates tick across all stores via read → compute → write pipeline.
|
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
|
import { HOURS_PER_TICK, MAX_DAY } from '../constants';
|
||||||
@@ -52,7 +49,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
...initialState,
|
...initialState,
|
||||||
|
|
||||||
initGame: () => {
|
initGame: () => {
|
||||||
// Wire discipline store ↔ combat store callbacks (breaks circular dependency)
|
|
||||||
useDisciplineStore.getState().setPracticingCallbacks({
|
useDisciplineStore.getState().setPracticingCallbacks({
|
||||||
onStartPracticing: () => useCombatStore.getState().startPracticing(),
|
onStartPracticing: () => useCombatStore.getState().startPracticing(),
|
||||||
onStopPracticing: () => useCombatStore.getState().stopPracticing(),
|
onStopPracticing: () => useCombatStore.getState().stopPracticing(),
|
||||||
@@ -62,7 +58,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
tick: () => {
|
tick: () => {
|
||||||
try {
|
try {
|
||||||
// ── Phase 1: Read — snapshot all store states once ──────────────────
|
|
||||||
const ctx = buildTickContext({
|
const ctx = buildTickContext({
|
||||||
game: get(),
|
game: get(),
|
||||||
ui: useUIStore.getState(),
|
ui: useUIStore.getState(),
|
||||||
@@ -76,7 +71,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
if (ctx.ui.gameOver || ctx.ui.paused) return;
|
if (ctx.ui.gameOver || ctx.ui.paused) return;
|
||||||
|
|
||||||
// Shared setters object — used by every applyTickWrites call below
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const storeSetters = {
|
const storeSetters = {
|
||||||
setGame: set,
|
setGame: set,
|
||||||
@@ -90,11 +84,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Phase 2: Compute — derive all updates ───────────────────────────
|
|
||||||
const writes: TickWrites = { logs: [] };
|
const writes: TickWrites = { logs: [] };
|
||||||
const addLog = (msg: string) => writes.logs.push(msg);
|
const addLog = (msg: string) => writes.logs.push(msg);
|
||||||
|
|
||||||
// Compute equipment and discipline effects
|
|
||||||
const steadyHandLevel = ctx.prestige.prestigeUpgrades.steadyHand || 0;
|
const steadyHandLevel = ctx.prestige.prestigeUpgrades.steadyHand || 0;
|
||||||
const enchantmentPowerMultiplier = 1 + steadyHandLevel * 0.15;
|
const enchantmentPowerMultiplier = 1 + steadyHandLevel * 0.15;
|
||||||
const equipmentEffects = computeEquipmentEffects(
|
const equipmentEffects = computeEquipmentEffects(
|
||||||
@@ -120,7 +112,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
disciplineEffects,
|
disciplineEffects,
|
||||||
) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0));
|
) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0));
|
||||||
|
|
||||||
// Time progression
|
|
||||||
let hour = ctx.game.hour + HOURS_PER_TICK;
|
let hour = ctx.game.hour + HOURS_PER_TICK;
|
||||||
let day = ctx.game.day;
|
let day = ctx.game.day;
|
||||||
if (hour >= 24) {
|
if (hour >= 24) {
|
||||||
@@ -128,7 +119,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
day += 1;
|
day += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared insight params — reused for both loop-end and victory
|
|
||||||
const insightParams = {
|
const insightParams = {
|
||||||
maxFloorReached: ctx.combat.maxFloorReached,
|
maxFloorReached: ctx.combat.maxFloorReached,
|
||||||
totalManaGathered: ctx.mana.totalManaGathered,
|
totalManaGathered: ctx.mana.totalManaGathered,
|
||||||
@@ -136,11 +126,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for loop end
|
|
||||||
if (day > MAX_DAY) {
|
if (day > MAX_DAY) {
|
||||||
const insightGained = calcInsight(insightParams, disciplineEffects);
|
const insightGained = calcInsight(insightParams, disciplineEffects);
|
||||||
|
addLog('The loop ends. Gained ' + insightGained + ' Insight.');
|
||||||
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
|
||||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
|
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
|
||||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||||
writes.game = { day, hour };
|
writes.game = { day, hour };
|
||||||
@@ -148,11 +136,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for victory (3× insight multiplier)
|
|
||||||
if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
|
if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
|
||||||
const insightGained = calcInsight(insightParams, disciplineEffects) * 3;
|
const insightGained = calcInsight(insightParams, disciplineEffects) * 3;
|
||||||
|
addLog('VICTORY! The Awakened One falls! Gained ' + insightGained + ' Insight!');
|
||||||
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
|
||||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
|
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
|
||||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||||
applyTickWrites(writes, storeSetters);
|
applyTickWrites(writes, storeSetters);
|
||||||
@@ -161,7 +147,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
const incursionStrength = getIncursionStrength(day, hour);
|
const incursionStrength = getIncursionStrength(day, hour);
|
||||||
|
|
||||||
// Meditation bonus tracking
|
|
||||||
let meditateTicks = ctx.mana.meditateTicks;
|
let meditateTicks = ctx.mana.meditateTicks;
|
||||||
let meditationMultiplier = 1;
|
let meditationMultiplier = 1;
|
||||||
|
|
||||||
@@ -172,7 +157,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
meditateTicks = 0;
|
meditateTicks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total attunement conversion and apply to element pools
|
|
||||||
let totalConversionPerTick = 0;
|
let totalConversionPerTick = 0;
|
||||||
let elements = { ...ctx.mana.elements };
|
let elements = { ...ctx.mana.elements };
|
||||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||||
@@ -195,11 +179,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||||
|
|
||||||
// Mana regeneration
|
|
||||||
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||||
let totalManaGathered = ctx.mana.totalManaGathered;
|
let totalManaGathered = ctx.mana.totalManaGathered;
|
||||||
|
|
||||||
// Convert action
|
|
||||||
if (ctx.combat.currentAction === 'convert') {
|
if (ctx.combat.currentAction === 'convert') {
|
||||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||||
if (convertResult) {
|
if (convertResult) {
|
||||||
@@ -208,7 +190,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pact ritual
|
|
||||||
const pactResult = processPactRitual(
|
const pactResult = processPactRitual(
|
||||||
ctx.prestige.pactRitualFloor,
|
ctx.prestige.pactRitualFloor,
|
||||||
ctx.prestige.pactRitualProgress,
|
ctx.prestige.pactRitualProgress,
|
||||||
@@ -222,7 +203,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
pactResult.logs.forEach(l => addLog(l));
|
pactResult.logs.forEach(l => addLog(l));
|
||||||
|
|
||||||
// Discipline tick
|
|
||||||
const disciplineResult = useDisciplineStore.getState().processTick({
|
const disciplineResult = useDisciplineStore.getState().processTick({
|
||||||
rawMana,
|
rawMana,
|
||||||
elements,
|
elements,
|
||||||
@@ -230,69 +210,50 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
rawMana = disciplineResult.rawMana;
|
rawMana = disciplineResult.rawMana;
|
||||||
elements = disciplineResult.elements;
|
elements = disciplineResult.elements;
|
||||||
|
|
||||||
// Apply discipline conversions: drain source mana, add to target element
|
|
||||||
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
||||||
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
||||||
// Check that all source mana types are available (unlocked and have enough)
|
|
||||||
let canConvert = true;
|
let canConvert = true;
|
||||||
for (const srcType of conv.sourceManaTypes) {
|
for (const srcType of conv.sourceManaTypes) {
|
||||||
if (srcType === 'raw') {
|
if (srcType === 'raw') {
|
||||||
if (rawMana < conversionAmount) {
|
if (rawMana < conversionAmount) { canConvert = false; break; }
|
||||||
canConvert = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (!elements[srcType] || !elements[srcType].unlocked || elements[srcType].current < conversionAmount) {
|
} else if (!elements[srcType] || !elements[srcType].unlocked || elements[srcType].current < conversionAmount) {
|
||||||
canConvert = false;
|
canConvert = false; break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!canConvert) continue;
|
if (!canConvert) continue;
|
||||||
// Drain source mana types
|
|
||||||
for (const srcType of conv.sourceManaTypes) {
|
for (const srcType of conv.sourceManaTypes) {
|
||||||
if (srcType === 'raw') {
|
if (srcType === 'raw') {
|
||||||
rawMana -= conversionAmount;
|
rawMana -= conversionAmount;
|
||||||
} else if (elements[srcType]) {
|
} else if (elements[srcType]) {
|
||||||
elements[srcType] = {
|
elements[srcType] = { ...elements[srcType], current: elements[srcType].current - conversionAmount };
|
||||||
...elements[srcType],
|
|
||||||
current: elements[srcType].current - conversionAmount,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add to target element
|
|
||||||
if (elements[targetElem]) {
|
if (elements[targetElem]) {
|
||||||
elements[targetElem] = {
|
elements[targetElem] = {
|
||||||
...elements[targetElem],
|
...elements[targetElem],
|
||||||
current: Math.min(
|
current: Math.min(elements[targetElem].max, elements[targetElem].current + conversionAmount),
|
||||||
elements[targetElem].max,
|
|
||||||
elements[targetElem].current + conversionAmount,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unlock enchantment effects from newly unlocked discipline perks
|
|
||||||
if (disciplineResult.unlockedEffects.length > 0) {
|
if (disciplineResult.unlockedEffects.length > 0) {
|
||||||
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
||||||
for (const effectId of disciplineResult.unlockedEffects) {
|
for (const effectId of disciplineResult.unlockedEffects) {
|
||||||
addLog(`✨ Discipline insight unlocked: ${effectId}`);
|
addLog('Discipline insight unlocked: ' + effectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlock fabricator recipes from newly unlocked discipline perks
|
|
||||||
if (disciplineResult.unlockedRecipes.length > 0) {
|
if (disciplineResult.unlockedRecipes.length > 0) {
|
||||||
useCraftingStore.getState().unlockRecipes(disciplineResult.unlockedRecipes);
|
useCraftingStore.getState().unlockRecipes(disciplineResult.unlockedRecipes);
|
||||||
for (const recipeId of disciplineResult.unlockedRecipes) {
|
for (const recipeId of disciplineResult.unlockedRecipes) {
|
||||||
addLog(`🔨 Fabricator recipe unlocked: ${recipeId}`);
|
addLog('Fabricator recipe unlocked: ' + recipeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply per-element capacity bonuses from disciplines and equipment
|
|
||||||
const perElementCapBonuses = mergePerElementCapBonuses(
|
const perElementCapBonuses = mergePerElementCapBonuses(
|
||||||
disciplineEffects.bonuses,
|
disciplineEffects.bonuses,
|
||||||
equipmentEffects.bonuses,
|
equipmentEffects.bonuses,
|
||||||
);
|
);
|
||||||
useManaStore.getState().computeElementMaxWithBonuses(perElementCapBonuses);
|
useManaStore.getState().computeElementMaxWithBonuses(perElementCapBonuses);
|
||||||
|
|
||||||
// Sync updated max/baseMax from mana store into tick elements snapshot
|
|
||||||
const manaStateAfter = useManaStore.getState();
|
const manaStateAfter = useManaStore.getState();
|
||||||
for (const [ek, es] of Object.entries(manaStateAfter.elements)) {
|
for (const [ek, es] of Object.entries(manaStateAfter.elements)) {
|
||||||
if (elements[ek]) {
|
if (elements[ek]) {
|
||||||
@@ -310,10 +271,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
(floor, wasGuardian) => {
|
(floor, wasGuardian) => {
|
||||||
if (wasGuardian) {
|
if (wasGuardian) {
|
||||||
const defeatedGuardian = getGuardianForFloor(floor);
|
const defeatedGuardian = getGuardianForFloor(floor);
|
||||||
addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
|
addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
||||||
} else if (floor % 5 === 0) {
|
} else if (floor % 5 === 0) {
|
||||||
addLog(`🏰 Floor ${floor} cleared!`);
|
addLog('Floor ' + floor + ' cleared!');
|
||||||
}
|
}
|
||||||
|
useCombatStore.setState({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 });
|
||||||
},
|
},
|
||||||
(damage) => {
|
(damage) => {
|
||||||
let dmg = damage;
|
let dmg = damage;
|
||||||
@@ -323,6 +285,46 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||||
dmg *= 1.5;
|
dmg *= 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||||
|
if (guardian && (guardian.shield || guardian.barrier || guardian.healthRegen)) {
|
||||||
|
let shield = ctx.combat.guardianShield;
|
||||||
|
let shieldMax = ctx.combat.guardianShieldMax;
|
||||||
|
let barrier = ctx.combat.guardianBarrier;
|
||||||
|
let barrierMax = ctx.combat.guardianBarrierMax;
|
||||||
|
|
||||||
|
if (guardian.shieldRegen && shield < shieldMax) {
|
||||||
|
shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK);
|
||||||
|
}
|
||||||
|
if (guardian.barrierRegen && barrier < barrierMax) {
|
||||||
|
barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shield > 0 && dmg > 0) {
|
||||||
|
const absorb = Math.min(shield, dmg);
|
||||||
|
shield -= absorb;
|
||||||
|
dmg -= absorb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrier > 0 && dmg > 0) {
|
||||||
|
dmg *= (1 - barrier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guardian.healthRegen && guardian.healthRegen > 0) {
|
||||||
|
const healAmount = guardian.healthRegenIsPercent
|
||||||
|
? Math.floor(ctx.combat.floorMaxHP * guardian.healthRegen / 100 * HOURS_PER_TICK)
|
||||||
|
: Math.floor(guardian.healthRegen * HOURS_PER_TICK);
|
||||||
|
dmg -= healAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
useCombatStore.setState({
|
||||||
|
guardianShield: shield,
|
||||||
|
guardianShieldMax: shieldMax,
|
||||||
|
guardianBarrier: barrier,
|
||||||
|
guardianBarrierMax: barrierMax,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { rawMana, elements, modifiedDamage: dmg };
|
return { rawMana, elements, modifiedDamage: dmg };
|
||||||
},
|
},
|
||||||
ctx.prestige.signedPacts,
|
ctx.prestige.signedPacts,
|
||||||
@@ -347,7 +349,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equipment crafting tick — advance progress and complete when done
|
|
||||||
if (ctx.combat.currentAction === 'craft') {
|
if (ctx.combat.currentAction === 'craft') {
|
||||||
const craftingResult = useCraftingStore.getState().processEquipmentCraftingTick();
|
const craftingResult = useCraftingStore.getState().processEquipmentCraftingTick();
|
||||||
if (craftingResult.logMessage) {
|
if (craftingResult.logMessage) {
|
||||||
@@ -355,7 +356,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 3: Write — batch all state updates ─────────────────────────
|
// Phase 3: Write
|
||||||
writes.game = { day, hour, incursionStrength };
|
writes.game = { day, hour, incursionStrength };
|
||||||
writes.mana = {
|
writes.mana = {
|
||||||
rawMana,
|
rawMana,
|
||||||
@@ -366,10 +367,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
applyTickWrites(writes, storeSetters);
|
applyTickWrites(writes, storeSetters);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Log error to UI store if available, otherwise console error
|
|
||||||
try {
|
try {
|
||||||
const msg = error instanceof Error ? error.message : String(error);
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
useUIStore.getState().addLog(`⚠️ Tick error: ${msg}`);
|
useUIStore.getState().addLog('Tick error: ' + msg);
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Tick error:', error);
|
console.error('Tick error:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ export interface GuardianDef {
|
|||||||
pactTime: number; // Hours required for pact ritual
|
pactTime: number; // Hours required for pact ritual
|
||||||
uniquePerk: string; // Description of unique perk
|
uniquePerk: string; // Description of unique perk
|
||||||
armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
|
armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
|
||||||
|
shield?: number; // Flat damage absorption pool (absorbs damage before HP)
|
||||||
|
shieldRegen?: number; // Shield regeneration per tick (flat amount)
|
||||||
|
barrier?: number; // Percentage-based damage reduction (0-1, e.g., 0.1 = 10%) that absorbs damage before HP
|
||||||
|
barrierRegen?: number; // Barrier regeneration per tick (percentage of max barrier)
|
||||||
|
healthRegen?: number; // Health regeneration per tick (flat amount, can be percentage-based with healthRegenIsPercent)
|
||||||
|
healthRegenIsPercent?: boolean; // If true, healthRegen is % of max HP per tick; if false, flat HP per tick
|
||||||
power: number; // Combat power for display
|
power: number; // Combat power for display
|
||||||
effects: { type: string; value: number }[]; // Passive combat effects
|
effects: { type: string; value: number }[]; // Passive combat effects
|
||||||
signingCost: { mana: number; time: number }; // Pact ritual cost & time
|
signingCost: { mana: number; time: number }; // Pact ritual cost & time
|
||||||
|
|||||||
Reference in New Issue
Block a user