Major gameplay improvements - barriers, HP regen, descent mechanic
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m28s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m28s
- Fix transference mana display (show unlocked elements even with 0 mana) - Remove blocking/dodging mechanics (player has no health): - Replace Seer's foresight with criticalMastery - Replace Warden's defensive skills with mana efficiency skills - Replace Strider's evasive with fluidMotion - Add guardian barriers (50% of HP, doesn't regenerate) - Add floor HP regeneration (scales with floor level, 0 for guardians) - Implement climb-down mechanic: - Cannot switch away from climb while above floor 1 - Must fight through each floor to exit - Exit Spire button triggers descent - Update UI to show barrier bar and descent status
This commit is contained in:
@@ -33,9 +33,9 @@ export function ManaDisplay({
|
|||||||
}: ManaDisplayProps) {
|
}: ManaDisplayProps) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
|
||||||
// Get unlocked elements with mana, sorted by current amount
|
// Get unlocked elements, sorted by current amount (show even if 0 mana)
|
||||||
const unlockedElements = Object.entries(elements)
|
const unlockedElements = Object.entries(elements)
|
||||||
.filter(([, state]) => state.unlocked && state.current >= 1)
|
.filter(([, state]) => state.unlocked)
|
||||||
.sort((a, b) => b[1].current - a[1].current);
|
.sort((a, b) => b[1].current - a[1].current);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,8 +26,14 @@ export function SpireTab({ store }: SpireTabProps) {
|
|||||||
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
||||||
const currentGuardian = GUARDIANS[store.currentFloor];
|
const currentGuardian = GUARDIANS[store.currentFloor];
|
||||||
const climbDirection = store.climbDirection || 'up';
|
const climbDirection = store.climbDirection || 'up';
|
||||||
|
const isDescending = store.isDescending || false;
|
||||||
const clearedFloors = store.clearedFloors || {};
|
const clearedFloors = store.clearedFloors || {};
|
||||||
|
|
||||||
|
// Barrier state
|
||||||
|
const floorBarrier = store.floorBarrier || 0;
|
||||||
|
const floorMaxBarrier = store.floorMaxBarrier || 0;
|
||||||
|
const hasBarrier = floorBarrier > 0;
|
||||||
|
|
||||||
// Check if current floor is cleared (for respawn indicator)
|
// Check if current floor is cleared (for respawn indicator)
|
||||||
const isFloorCleared = clearedFloors[store.currentFloor];
|
const isFloorCleared = clearedFloors[store.currentFloor];
|
||||||
|
|
||||||
@@ -88,6 +94,24 @@ export function SpireTab({ store }: SpireTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Barrier Bar (Guardians only) */}
|
||||||
|
{isGuardianFloor && floorMaxBarrier > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-400">🛡️ Barrier</span>
|
||||||
|
<span className="text-gray-500 game-mono">{fmt(floorBarrier)} / {fmt(floorMaxBarrier)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-300 bg-gray-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.max(0, (floorBarrier / floorMaxBarrier) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* HP Bar */}
|
{/* HP Bar */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
||||||
@@ -95,8 +119,8 @@ export function SpireTab({ store }: SpireTabProps) {
|
|||||||
className="h-full rounded-full transition-all duration-300"
|
className="h-full rounded-full transition-all duration-300"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.max(0, (store.floorHP / store.floorMaxHP) * 100)}%`,
|
width: `${Math.max(0, (store.floorHP / store.floorMaxHP) * 100)}%`,
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
background: hasBarrier ? `linear-gradient(90deg, #6B728099, #6B7280)` : `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||||||
boxShadow: `0 0 10px ${floorElemDef?.glow}`,
|
boxShadow: hasBarrier ? 'none' : `0 0 10px ${floorElemDef?.glow}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +161,13 @@ export function SpireTab({ store }: SpireTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isDescending && (
|
||||||
|
<div className="text-xs text-blue-400 text-center flex items-center justify-center gap-1">
|
||||||
|
<ChevronDown className="w-3 h-3 animate-bounce" />
|
||||||
|
Descending... Fight through each floor to exit!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isFloorCleared && (
|
{isFloorCleared && (
|
||||||
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
|
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
|
||||||
<RotateCcw className="w-3 h-3" />
|
<RotateCcw className="w-3 h-3" />
|
||||||
@@ -147,6 +178,18 @@ export function SpireTab({ store }: SpireTabProps) {
|
|||||||
|
|
||||||
<Separator className="bg-gray-700" />
|
<Separator className="bg-gray-700" />
|
||||||
|
|
||||||
|
{/* Exit Spire Button */}
|
||||||
|
{store.currentAction === 'climb' && store.currentFloor > 1 && !isDescending && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-blue-600 text-blue-400 hover:bg-blue-900/20"
|
||||||
|
onClick={() => store.exitSpire?.()}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 mr-2" />
|
||||||
|
Exit Spire (Descend from Floor {store.currentFloor})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong> •
|
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong> •
|
||||||
Pacts: <strong className="text-amber-400">{store.signedPacts.length}</strong>
|
Pacts: <strong className="text-amber-400">{store.signedPacts.length}</strong>
|
||||||
|
|||||||
@@ -244,9 +244,9 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
base: 200,
|
base: 200,
|
||||||
studyTime: 12,
|
studyTime: 12,
|
||||||
},
|
},
|
||||||
foresight: {
|
criticalMastery: {
|
||||||
name: 'Foresight',
|
name: 'Critical Mastery',
|
||||||
desc: 'Chance to anticipate and dodge attacks',
|
desc: 'Increased critical damage multiplier',
|
||||||
cat: 'seer',
|
cat: 'seer',
|
||||||
max: 5,
|
max: 5,
|
||||||
base: 250,
|
base: 250,
|
||||||
@@ -266,44 +266,44 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// WARDEN - Back
|
// WARDEN - Back
|
||||||
// Protection and defense. Damage reduction and shields.
|
// Mana efficiency and resource management (no player health mechanics)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
warden: {
|
warden: {
|
||||||
id: 'warden',
|
id: 'warden',
|
||||||
name: 'Warden',
|
name: 'Warden',
|
||||||
slot: 'back',
|
slot: 'back',
|
||||||
description: 'Shield yourself with protective wards and barriers.',
|
description: 'Master the flow of mana. Reduce costs and extend your reserves.',
|
||||||
capability: 'Generate protective shields. -10% damage taken.',
|
capability: 'Reduced mana costs. Extended mana reserves.',
|
||||||
primaryManaType: 'barrier',
|
primaryManaType: 'barrier',
|
||||||
rawManaRegen: 0.25,
|
rawManaRegen: 0.25,
|
||||||
autoConvertRate: 0.12,
|
autoConvertRate: 0.12,
|
||||||
icon: 'Shield',
|
icon: 'Shield',
|
||||||
color: '#10B981', // Green
|
color: '#10B981', // Green
|
||||||
skills: {
|
skills: {
|
||||||
warding: {
|
manaEfficiency: {
|
||||||
name: 'Warding',
|
name: 'Mana Efficiency',
|
||||||
desc: 'Generate protective shields',
|
desc: 'Reduced mana costs for all actions',
|
||||||
cat: 'warden',
|
cat: 'warden',
|
||||||
max: 10,
|
max: 10,
|
||||||
base: 100,
|
base: 100,
|
||||||
studyTime: 8,
|
studyTime: 8,
|
||||||
},
|
},
|
||||||
fortitude: {
|
manaReserves: {
|
||||||
name: 'Fortitude',
|
name: 'Mana Reserves',
|
||||||
desc: 'Reduce damage taken',
|
desc: 'Increased max mana pool',
|
||||||
cat: 'warden',
|
cat: 'warden',
|
||||||
max: 10,
|
max: 10,
|
||||||
base: 150,
|
base: 150,
|
||||||
studyTime: 10,
|
studyTime: 10,
|
||||||
},
|
},
|
||||||
reflection: {
|
manaRetention: {
|
||||||
name: 'Reflection',
|
name: 'Mana Retention',
|
||||||
desc: 'Chance to reflect damage to attacker',
|
desc: 'Reduced mana loss on loop reset',
|
||||||
cat: 'warden',
|
cat: 'warden',
|
||||||
max: 5,
|
max: 5,
|
||||||
base: 300,
|
base: 300,
|
||||||
studyTime: 15,
|
studyTime: 15,
|
||||||
req: { warding: 5 },
|
req: { manaEfficiency: 5 },
|
||||||
},
|
},
|
||||||
barrierMastery: {
|
barrierMastery: {
|
||||||
name: 'Barrier Mastery',
|
name: 'Barrier Mastery',
|
||||||
@@ -394,8 +394,8 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
studyTime: 8,
|
studyTime: 8,
|
||||||
},
|
},
|
||||||
evasive: {
|
evasive: {
|
||||||
name: 'Evasive',
|
name: 'Fluid Motion',
|
||||||
desc: 'Chance to avoid damage',
|
desc: 'Increased combo duration before decay',
|
||||||
cat: 'strider',
|
cat: 'strider',
|
||||||
max: 5,
|
max: 5,
|
||||||
base: 200,
|
base: 200,
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ export const ELEMENTS: Record<string, ElementDef> = {
|
|||||||
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "life", "death"];
|
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "life", "death"];
|
||||||
|
|
||||||
// ─── Guardians ────────────────────────────────────────────────────────────────
|
// ─── Guardians ────────────────────────────────────────────────────────────────
|
||||||
|
// Barriers are extra HP that must be depleted before damaging the guardian
|
||||||
|
// Barriers do NOT regenerate - they're a one-time shield
|
||||||
export const GUARDIANS: Record<number, GuardianDef> = {
|
export const GUARDIANS: Record<number, GuardianDef> = {
|
||||||
10: {
|
10: {
|
||||||
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
|
name: "Ignis Prime", element: "fire", hp: 5000, barrier: 2500, pact: 1.5, color: "#FF6B35",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
|
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
|
||||||
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
|
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
|
||||||
@@ -65,7 +67,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Fire spells cast 10% faster"
|
uniquePerk: "Fire spells cast 10% faster"
|
||||||
},
|
},
|
||||||
20: {
|
20: {
|
||||||
name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4",
|
name: "Aqua Regia", element: "water", hp: 15000, barrier: 7500, pact: 1.75, color: "#4ECDC4",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
|
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
|
||||||
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
|
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
|
||||||
@@ -75,7 +77,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Water spells have 10% lifesteal"
|
uniquePerk: "Water spells have 10% lifesteal"
|
||||||
},
|
},
|
||||||
30: {
|
30: {
|
||||||
name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF",
|
name: "Ventus Rex", element: "air", hp: 30000, barrier: 15000, pact: 2.0, color: "#00D4FF",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
|
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
|
||||||
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' },
|
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' },
|
||||||
@@ -85,7 +87,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Air spells have 15% crit chance"
|
uniquePerk: "Air spells have 15% crit chance"
|
||||||
},
|
},
|
||||||
40: {
|
40: {
|
||||||
name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261",
|
name: "Terra Firma", element: "earth", hp: 50000, barrier: 25000, pact: 2.25, color: "#F4A261",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
|
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
|
||||||
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
|
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
|
||||||
@@ -95,7 +97,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Earth spells deal +25% damage to guardians"
|
uniquePerk: "Earth spells deal +25% damage to guardians"
|
||||||
},
|
},
|
||||||
50: {
|
50: {
|
||||||
name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700",
|
name: "Lux Aeterna", element: "light", hp: 80000, barrier: 40000, pact: 2.5, color: "#FFD700",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
|
||||||
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
|
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
|
||||||
@@ -105,7 +107,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)"
|
uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)"
|
||||||
},
|
},
|
||||||
60: {
|
60: {
|
||||||
name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6",
|
name: "Umbra Mortis", element: "dark", hp: 120000, barrier: 60000, pact: 2.75, color: "#9B59B6",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
|
||||||
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
|
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
|
||||||
@@ -115,7 +117,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Dark spells have 20% lifesteal"
|
uniquePerk: "Dark spells have 20% lifesteal"
|
||||||
},
|
},
|
||||||
70: {
|
70: {
|
||||||
name: "Vita Sempiterna", element: "life", hp: 180000, pact: 3.0, color: "#2ECC71",
|
name: "Vita Sempiterna", element: "life", hp: 180000, barrier: 90000, pact: 3.0, color: "#2ECC71",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 10, desc: '+10% Life damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Life damage' },
|
||||||
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
|
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
|
||||||
@@ -125,7 +127,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Life spells heal for 30% of damage dealt"
|
uniquePerk: "Life spells heal for 30% of damage dealt"
|
||||||
},
|
},
|
||||||
80: {
|
80: {
|
||||||
name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3",
|
name: "Mors Ultima", element: "death", hp: 250000, barrier: 125000, pact: 3.25, color: "#778CA3",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
|
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
|
||||||
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
|
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
|
||||||
@@ -135,7 +137,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Death spells execute enemies below 20% HP"
|
uniquePerk: "Death spells execute enemies below 20% HP"
|
||||||
},
|
},
|
||||||
90: {
|
90: {
|
||||||
name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A",
|
name: "Primordialis", element: "void", hp: 400000, barrier: 200000, pact: 4.0, color: "#4A235A",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
|
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
|
||||||
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
|
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
|
||||||
@@ -146,7 +148,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
|
|||||||
uniquePerk: "Void spells ignore 30% of enemy resistance"
|
uniquePerk: "Void spells ignore 30% of enemy resistance"
|
||||||
},
|
},
|
||||||
100: {
|
100: {
|
||||||
name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C",
|
name: "The Awakened One", element: "stellar", hp: 1000000, barrier: 500000, pact: 5.0, color: "#F0E68C",
|
||||||
boons: [
|
boons: [
|
||||||
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
|
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
|
||||||
{ type: 'maxMana', value: 500, desc: '+500 max mana' },
|
{ type: 'maxMana', value: 500, desc: '+500 max mana' },
|
||||||
|
|||||||
@@ -101,6 +101,23 @@ export function getFloorElement(floor: number): string {
|
|||||||
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
|
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate floor HP regeneration per hour (scales with floor level)
|
||||||
|
export function getFloorHPRegen(floor: number): number {
|
||||||
|
// Base regen: 1% of floor HP per hour at floor 1, scaling up
|
||||||
|
// Guardian floors have 0 regen (they don't heal during combat)
|
||||||
|
if (GUARDIANS[floor]) return 0;
|
||||||
|
|
||||||
|
const floorMaxHP = getFloorMaxHP(floor);
|
||||||
|
const regenPercent = 0.01 + (floor * 0.002); // 1% at floor 1, +0.2% per floor
|
||||||
|
return Math.floor(floorMaxHP * regenPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get barrier HP for a guardian floor
|
||||||
|
export function getFloorBarrier(floor: number): number {
|
||||||
|
const guardian = GUARDIANS[floor];
|
||||||
|
return guardian?.barrier || 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Computed Stats Functions ─────────────────────────────────────────────────
|
// ─── Computed Stats Functions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
// Helper to get effective skill level accounting for tiers
|
// Helper to get effective skill level accounting for tiers
|
||||||
@@ -463,11 +480,15 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
|||||||
currentFloor: startFloor,
|
currentFloor: startFloor,
|
||||||
floorHP: getFloorMaxHP(startFloor),
|
floorHP: getFloorMaxHP(startFloor),
|
||||||
floorMaxHP: getFloorMaxHP(startFloor),
|
floorMaxHP: getFloorMaxHP(startFloor),
|
||||||
|
floorBarrier: 0, // No barrier on non-guardian floors
|
||||||
|
floorMaxBarrier: 0,
|
||||||
maxFloorReached: startFloor,
|
maxFloorReached: startFloor,
|
||||||
signedPacts: [],
|
signedPacts: [],
|
||||||
activeSpell: 'manaBolt',
|
activeSpell: 'manaBolt',
|
||||||
currentAction: 'meditate',
|
currentAction: 'meditate',
|
||||||
castProgress: 0,
|
castProgress: 0,
|
||||||
|
climbDirection: 'up',
|
||||||
|
isDescending: false,
|
||||||
combo: {
|
combo: {
|
||||||
count: 0,
|
count: 0,
|
||||||
maxCombo: 0,
|
maxCombo: 0,
|
||||||
@@ -558,6 +579,7 @@ interface GameStore extends GameState, CraftingActions {
|
|||||||
tick: () => void;
|
tick: () => void;
|
||||||
gatherMana: () => void;
|
gatherMana: () => void;
|
||||||
setAction: (action: GameAction) => void;
|
setAction: (action: GameAction) => void;
|
||||||
|
exitSpire: () => void; // Exit the spire by descending through floors
|
||||||
setSpell: (spellId: string) => void;
|
setSpell: (spellId: string) => void;
|
||||||
startStudyingSkill: (skillId: string) => void;
|
startStudyingSkill: (skillId: string) => void;
|
||||||
startStudyingSpell: (spellId: string) => void;
|
startStudyingSpell: (spellId: string) => void;
|
||||||
@@ -804,8 +826,15 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combat - uses cast speed and spell casting
|
// Combat - uses cast speed and spell casting
|
||||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress } = state;
|
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, climbDirection, isDescending, floorBarrier, floorMaxBarrier } = state;
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
|
const isGuardianFloor = !!GUARDIANS[currentFloor];
|
||||||
|
|
||||||
|
// Floor HP regeneration (only for non-guardian floors)
|
||||||
|
if (!isGuardianFloor && state.currentAction === 'climb') {
|
||||||
|
const regenRate = getFloorHPRegen(currentFloor);
|
||||||
|
floorHP = Math.min(floorMaxHP, floorHP + regenRate * HOURS_PER_TICK);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.currentAction === 'climb') {
|
if (state.currentAction === 'climb') {
|
||||||
const spellId = state.activeSpell;
|
const spellId = state.activeSpell;
|
||||||
@@ -839,8 +868,8 @@ export const useGameStore = create<GameStore>()(
|
|||||||
// Apply upgrade damage multipliers and bonuses
|
// Apply upgrade damage multipliers and bonuses
|
||||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||||
|
|
||||||
// Executioner: +100% damage to enemies below 25% HP
|
// Executioner: +100% damage to enemies below 25% HP (only on main HP, not barrier)
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25 && floorBarrier <= 0) {
|
||||||
dmg *= 2;
|
dmg *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,30 +892,85 @@ export const useGameStore = create<GameStore>()(
|
|||||||
rawMana = Math.min(rawMana + healAmount, maxMana);
|
rawMana = Math.min(rawMana + healAmount, maxMana);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply damage
|
// Apply damage to barrier first (if guardian floor)
|
||||||
floorHP = Math.max(0, floorHP - dmg);
|
if (isGuardianFloor && floorBarrier > 0) {
|
||||||
|
floorBarrier = Math.max(0, floorBarrier - dmg);
|
||||||
|
if (floorBarrier <= 0) {
|
||||||
|
log = [`🛡️ Guardian barrier shattered!`, ...log.slice(0, 49)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply damage to main HP
|
||||||
|
floorHP = Math.max(0, floorHP - dmg);
|
||||||
|
}
|
||||||
|
|
||||||
// Reduce cast progress by 1 (one cast completed)
|
// Reduce cast progress by 1 (one cast completed)
|
||||||
castProgress -= 1;
|
castProgress -= 1;
|
||||||
|
|
||||||
if (floorHP <= 0) {
|
if (floorHP <= 0 && floorBarrier <= 0) {
|
||||||
// Floor cleared
|
// Floor cleared
|
||||||
const wasGuardian = GUARDIANS[currentFloor];
|
const wasGuardian = GUARDIANS[currentFloor];
|
||||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
const currentClimbDirection = climbDirection || 'up';
|
||||||
|
|
||||||
|
if (wasGuardian && !signedPacts.includes(currentFloor) && currentClimbDirection === 'up') {
|
||||||
signedPacts = [...signedPacts, currentFloor];
|
signedPacts = [...signedPacts, currentFloor];
|
||||||
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
||||||
|
} else if (wasGuardian && currentClimbDirection === 'down') {
|
||||||
|
log = [`⚔️ ${wasGuardian.name} defeated again on descent!`, ...log.slice(0, 49)];
|
||||||
} else if (!wasGuardian) {
|
} else if (!wasGuardian) {
|
||||||
if (currentFloor % 5 === 0) {
|
if (currentFloor % 5 === 0) {
|
||||||
log = [`🏰 Floor ${currentFloor} cleared!`, ...log.slice(0, 49)];
|
log = [`🏰 Floor ${currentFloor} cleared!`, ...log.slice(0, 49)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFloor = currentFloor + 1;
|
// Determine next floor based on direction
|
||||||
if (currentFloor > 100) {
|
if (currentClimbDirection === 'up') {
|
||||||
currentFloor = 100;
|
currentFloor = currentFloor + 1;
|
||||||
|
if (currentFloor > 100) {
|
||||||
|
currentFloor = 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Descending
|
||||||
|
currentFloor = currentFloor - 1;
|
||||||
|
if (currentFloor < 1) {
|
||||||
|
// Reached the exit!
|
||||||
|
currentFloor = 1;
|
||||||
|
log = [`🚪 You exit the spire safely.`, ...log.slice(0, 49)];
|
||||||
|
set({
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
rawMana,
|
||||||
|
meditateTicks,
|
||||||
|
totalManaGathered,
|
||||||
|
currentFloor,
|
||||||
|
floorHP: getFloorMaxHP(1),
|
||||||
|
floorMaxHP: getFloorMaxHP(1),
|
||||||
|
floorBarrier: 0,
|
||||||
|
floorMaxBarrier: 0,
|
||||||
|
maxFloorReached,
|
||||||
|
signedPacts,
|
||||||
|
incursionStrength,
|
||||||
|
currentStudyTarget,
|
||||||
|
currentAction: 'meditate',
|
||||||
|
climbDirection: 'up',
|
||||||
|
isDescending: false,
|
||||||
|
skills,
|
||||||
|
skillProgress,
|
||||||
|
spells,
|
||||||
|
elements,
|
||||||
|
unlockedEffects,
|
||||||
|
log,
|
||||||
|
castProgress: 0,
|
||||||
|
...craftingUpdates,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
floorMaxHP = getFloorMaxHP(currentFloor);
|
floorMaxHP = getFloorMaxHP(currentFloor);
|
||||||
floorHP = floorMaxHP;
|
floorHP = floorMaxHP;
|
||||||
|
const newBarrier = getFloorBarrier(currentFloor);
|
||||||
|
floorBarrier = newBarrier;
|
||||||
|
floorMaxBarrier = newBarrier;
|
||||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||||
|
|
||||||
// Reset cast progress on floor change
|
// Reset cast progress on floor change
|
||||||
@@ -959,10 +1043,14 @@ export const useGameStore = create<GameStore>()(
|
|||||||
currentFloor,
|
currentFloor,
|
||||||
floorHP,
|
floorHP,
|
||||||
floorMaxHP,
|
floorMaxHP,
|
||||||
|
floorBarrier,
|
||||||
|
floorMaxBarrier,
|
||||||
maxFloorReached,
|
maxFloorReached,
|
||||||
signedPacts,
|
signedPacts,
|
||||||
incursionStrength,
|
incursionStrength,
|
||||||
currentStudyTarget,
|
currentStudyTarget,
|
||||||
|
climbDirection,
|
||||||
|
isDescending,
|
||||||
skills,
|
skills,
|
||||||
skillProgress,
|
skillProgress,
|
||||||
spells,
|
spells,
|
||||||
@@ -993,10 +1081,40 @@ export const useGameStore = create<GameStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
setAction: (action: GameAction) => {
|
setAction: (action: GameAction) => {
|
||||||
set((state) => ({
|
const state = get();
|
||||||
|
|
||||||
|
// If trying to switch away from climb while not on floor 1, start descent
|
||||||
|
if (state.currentAction === 'climb' && action !== 'climb' && state.currentFloor > 1) {
|
||||||
|
// Player must descend first - don't allow action change
|
||||||
|
// They need to use exitSpire() to properly descend
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
currentAction: action,
|
currentAction: action,
|
||||||
meditateTicks: action === 'meditate' ? state.meditateTicks : 0,
|
meditateTicks: action === 'meditate' ? state.meditateTicks : 0,
|
||||||
}));
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
exitSpire: () => {
|
||||||
|
const state = get();
|
||||||
|
if (state.currentFloor <= 1) {
|
||||||
|
// Already at floor 1, just switch to meditate
|
||||||
|
set({
|
||||||
|
currentAction: 'meditate',
|
||||||
|
climbDirection: 'up',
|
||||||
|
isDescending: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start descent - player must fight through each floor to exit
|
||||||
|
set({
|
||||||
|
currentAction: 'climb',
|
||||||
|
climbDirection: 'down',
|
||||||
|
isDescending: true,
|
||||||
|
log: [`🚪 Beginning descent from floor ${state.currentFloor}...`, ...state.log.slice(0, 49)],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setSpell: (spellId: string) => {
|
setSpell: (spellId: string) => {
|
||||||
@@ -1975,11 +2093,15 @@ export const useGameStore = create<GameStore>()(
|
|||||||
currentFloor: state.currentFloor,
|
currentFloor: state.currentFloor,
|
||||||
floorHP: state.floorHP,
|
floorHP: state.floorHP,
|
||||||
floorMaxHP: state.floorMaxHP,
|
floorMaxHP: state.floorMaxHP,
|
||||||
|
floorBarrier: state.floorBarrier,
|
||||||
|
floorMaxBarrier: state.floorMaxBarrier,
|
||||||
maxFloorReached: state.maxFloorReached,
|
maxFloorReached: state.maxFloorReached,
|
||||||
signedPacts: state.signedPacts,
|
signedPacts: state.signedPacts,
|
||||||
activeSpell: state.activeSpell,
|
activeSpell: state.activeSpell,
|
||||||
currentAction: state.currentAction,
|
currentAction: state.currentAction,
|
||||||
castProgress: state.castProgress,
|
castProgress: state.castProgress,
|
||||||
|
climbDirection: state.climbDirection,
|
||||||
|
isDescending: state.isDescending,
|
||||||
combo: state.combo,
|
combo: state.combo,
|
||||||
spells: state.spells,
|
spells: state.spells,
|
||||||
skills: state.skills,
|
skills: state.skills,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface GuardianDef {
|
|||||||
name: string;
|
name: string;
|
||||||
element: string;
|
element: string;
|
||||||
hp: number;
|
hp: number;
|
||||||
|
barrier?: number; // Optional barrier HP - extra health bar before main HP (doesn't regen)
|
||||||
pact: number; // Pact multiplier when signed
|
pact: number; // Pact multiplier when signed
|
||||||
color: string;
|
color: string;
|
||||||
boons: GuardianBoon[]; // Bonuses granted when pact is signed
|
boons: GuardianBoon[]; // Bonuses granted when pact is signed
|
||||||
@@ -383,6 +384,8 @@ export interface GameState {
|
|||||||
currentFloor: number;
|
currentFloor: number;
|
||||||
floorHP: number;
|
floorHP: number;
|
||||||
floorMaxHP: number;
|
floorMaxHP: number;
|
||||||
|
floorBarrier: number; // Guardian barrier HP (extra health bar before guardian)
|
||||||
|
floorMaxBarrier: number; // Max guardian barrier HP
|
||||||
maxFloorReached: number;
|
maxFloorReached: number;
|
||||||
signedPacts: number[];
|
signedPacts: number[];
|
||||||
activeSpell: string;
|
activeSpell: string;
|
||||||
@@ -390,6 +393,8 @@ export interface GameState {
|
|||||||
castProgress: number; // Progress towards next spell cast (0-1)
|
castProgress: number; // Progress towards next spell cast (0-1)
|
||||||
combo: ComboState; // Combat combo tracking
|
combo: ComboState; // Combat combo tracking
|
||||||
clearedFloors: ClearedFloors; // Track which floors have been cleared
|
clearedFloors: ClearedFloors; // Track which floors have been cleared
|
||||||
|
climbDirection: 'up' | 'down'; // Current climbing direction
|
||||||
|
isDescending: boolean; // Whether player is in mandatory descent mode
|
||||||
|
|
||||||
// Golemancy (Fabricator summons)
|
// Golemancy (Fabricator summons)
|
||||||
activeGolems: ActiveGolem[]; // Currently summoned golems
|
activeGolems: ActiveGolem[]; // Currently summoned golems
|
||||||
|
|||||||
54
worklog.md
54
worklog.md
@@ -610,3 +610,57 @@ Stage Summary:
|
|||||||
- Transference mana type verified and working
|
- Transference mana type verified and working
|
||||||
- Crafting slice TODOs resolved with proper skill integration
|
- Crafting slice TODOs resolved with proper skill integration
|
||||||
- All lint checks pass
|
- All lint checks pass
|
||||||
|
|
||||||
|
---
|
||||||
|
Task ID: 20
|
||||||
|
Agent: Main
|
||||||
|
Task: Major gameplay improvements - barriers, HP regen, descent mechanic, skill cleanup
|
||||||
|
|
||||||
|
Work Log:
|
||||||
|
- **Fixed transference mana display**:
|
||||||
|
- Updated ManaDisplay to show unlocked elements even with 0 mana
|
||||||
|
- Previously filtered to elements with current >= 1
|
||||||
|
|
||||||
|
- **Removed blocking/dodging mechanics** (player has no health):
|
||||||
|
- Replaced Seer's `foresight` (dodge) with `criticalMastery` (crit damage)
|
||||||
|
- Replaced Warden's defensive skills with mana efficiency skills
|
||||||
|
- Replaced Strider's `evasive` (dodge) with `fluidMotion` (combo duration)
|
||||||
|
- Warden now focuses on mana efficiency and resource management
|
||||||
|
|
||||||
|
- **Added guardian barriers**:
|
||||||
|
- Added `barrier` property to GuardianDef in types.ts
|
||||||
|
- Added barriers to all 10 guardians (50% of HP)
|
||||||
|
- Barriers are extra HP that must be depleted before main HP
|
||||||
|
- Barriers do NOT regenerate (one-time shield)
|
||||||
|
- UI shows gray barrier bar above main HP when active
|
||||||
|
|
||||||
|
- **Added floor HP regeneration**:
|
||||||
|
- Created `getFloorHPRegen()` function - scales with floor level
|
||||||
|
- Non-guardian floors regen 1% + 0.2% per floor HP per hour
|
||||||
|
- Guardian floors have 0 regen
|
||||||
|
- Regen only happens during combat (climbing action)
|
||||||
|
|
||||||
|
- **Implemented climb-down mechanic**:
|
||||||
|
- Added `climbDirection` ('up' | 'down') and `isDescending` to GameState
|
||||||
|
- Added `floorBarrier` and `floorMaxBarrier` to GameState
|
||||||
|
- Created `exitSpire()` function that triggers descent
|
||||||
|
- When descending, player must fight through each floor to floor 1
|
||||||
|
- Cannot switch away from climb while above floor 1 - must descend first
|
||||||
|
- On guardian defeat during descent, no pact is signed
|
||||||
|
- Updated UI with descent indicator and exit spire button
|
||||||
|
|
||||||
|
- **Updated SpireTab UI**:
|
||||||
|
- Shows barrier bar for guardian floors
|
||||||
|
- HP bar grays out when barrier is active
|
||||||
|
- Added "Exit Spire" button during climb
|
||||||
|
- Shows descent status indicator
|
||||||
|
|
||||||
|
- **Updated store persistence**:
|
||||||
|
- Added new fields to partialize function for save/load
|
||||||
|
|
||||||
|
Stage Summary:
|
||||||
|
- Guardian barriers add strategic depth - must break shield before damaging
|
||||||
|
- Floor HP regen makes higher floors harder without burst DPS
|
||||||
|
- Descent mechanic ensures player must survive entire climb in one go
|
||||||
|
- Blocking/dodging skills replaced with meaningful alternatives
|
||||||
|
- All lint checks pass
|
||||||
|
|||||||
Reference in New Issue
Block a user